mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 00:28:42 +00:00
Restructure source directory
Introducing a directory structure to make it a bit less daunting for new developers and perhaps even old hands. The main folders all start with an upper character, so src files are now located in; * Core - Core data structures * Gui - Main GUI elements * Metrics - Models and Metrics * FileIO - Device and File I/O * Charts - All the chart types * Cloud - Working with Web Resources * Train - Anything Train View specific * ANT - Our ANT+ Stack * Resources - Images, Translations, Web etc Apologies to anyone who needs to merge across this update.
This commit is contained in:
533
src/Train/WorkoutWindow.cpp
Normal file
533
src/Train/WorkoutWindow.cpp
Normal file
@@ -0,0 +1,533 @@
|
||||
/*
|
||||
* 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 "WorkoutWindow.h"
|
||||
#include "WorkoutWidget.h"
|
||||
#include "WorkoutWidgetItems.h"
|
||||
|
||||
static int MINTOOLHEIGHT = 350; // smaller than this, lose the toolbar
|
||||
|
||||
WorkoutWindow::WorkoutWindow(Context *context) :
|
||||
GcWindow(context), draw(true), context(context), active(false), recording(false)
|
||||
{
|
||||
setContentsMargins(0,0,0,0);
|
||||
setProperty("color", GColor(CTRAINPLOTBACKGROUND));
|
||||
|
||||
setControls(NULL);
|
||||
ergFile = NULL;
|
||||
|
||||
QVBoxLayout *main = new QVBoxLayout(this);
|
||||
QHBoxLayout *layout = new QHBoxLayout;
|
||||
QVBoxLayout *editor = new QVBoxLayout;
|
||||
|
||||
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
|
||||
|
||||
// the workout scene
|
||||
workout = new WorkoutWidget(this, context);
|
||||
|
||||
// paint the TTE curve
|
||||
mmp = new WWMMPCurve(workout);
|
||||
|
||||
// add a line between the dots
|
||||
line = new WWLine(workout);
|
||||
|
||||
// block cursos
|
||||
bcursor = new WWBlockCursor(workout);
|
||||
|
||||
// block selection
|
||||
brect = new WWBlockSelection(workout);
|
||||
|
||||
// paint the W'bal curve
|
||||
wbline = new WWWBLine(workout, context);
|
||||
|
||||
// telemetry
|
||||
telemetry = new WWTelemetry(workout, context);
|
||||
|
||||
// add the power, W'bal scale
|
||||
powerscale = new WWPowerScale(workout, context);
|
||||
wbalscale = new WWWBalScale(workout, context);
|
||||
|
||||
// lap markers
|
||||
lap = new WWLap(workout);
|
||||
|
||||
// tte warning bar at bottom
|
||||
tte = new WWTTE(workout);
|
||||
|
||||
// selection tool
|
||||
rect = new WWRect(workout);
|
||||
|
||||
// guides always on top!
|
||||
guide = new WWSmartGuide(workout);
|
||||
|
||||
// recording ...
|
||||
now = new WWNow(workout, context);
|
||||
|
||||
// scroller, hidden until needed
|
||||
scroll = new QScrollBar(Qt::Horizontal, this);
|
||||
scroll->hide();
|
||||
|
||||
// setup the toolbar
|
||||
toolbar = new QToolBar(this);
|
||||
toolbar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
|
||||
toolbar->setFloatable(true);
|
||||
toolbar->setIconSize(QSize(18,18));
|
||||
|
||||
QIcon newIcon(":images/toolbar/new doc.png");
|
||||
newAct = new QAction(newIcon, tr("New"), this);
|
||||
connect(newAct, SIGNAL(triggered()), this, SLOT(newFile()));
|
||||
toolbar->addAction(newAct);
|
||||
|
||||
QIcon saveIcon(":images/toolbar/save.png");
|
||||
saveAct = new QAction(saveIcon, tr("Save"), this);
|
||||
connect(saveAct, SIGNAL(triggered()), this, SLOT(saveFile()));
|
||||
toolbar->addAction(saveAct);
|
||||
|
||||
QIcon saveAsIcon(":images/toolbar/saveas.png");
|
||||
saveAsAct = new QAction(saveAsIcon, tr("Save As"), this);
|
||||
connect(saveAsAct, SIGNAL(triggered()), this, SLOT(saveAs()));
|
||||
toolbar->addAction(saveAsAct);
|
||||
|
||||
toolbar->addSeparator();
|
||||
|
||||
//XXX TODO
|
||||
//XXXHelpWhatsThis *helpToolbar = new HelpWhatsThis(toolbar);
|
||||
//XXXtoolbar->setWhatsThis(helpToolbar->getWhatsThisText(HelpWhatsThis::ChartRides_Editor));
|
||||
|
||||
// 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()), workout, SLOT(undo()));
|
||||
toolbar->addAction(undoAct);
|
||||
|
||||
QIcon redoIcon(":images/toolbar/redo.png");
|
||||
redoAct = new QAction(redoIcon, tr("Redo"), this);
|
||||
connect(redoAct, SIGNAL(triggered()), workout, SLOT(redo()));
|
||||
toolbar->addAction(redoAct);
|
||||
|
||||
toolbar->addSeparator();
|
||||
|
||||
QIcon drawIcon(":images/toolbar/edit.png");
|
||||
drawAct = new QAction(drawIcon, tr("Draw"), this);
|
||||
connect(drawAct, SIGNAL(triggered()), this, SLOT(drawMode()));
|
||||
toolbar->addAction(drawAct);
|
||||
|
||||
QIcon selectIcon(":images/toolbar/select.png");
|
||||
selectAct = new QAction(selectIcon, tr("Select"), this);
|
||||
connect(selectAct, SIGNAL(triggered()), this, SLOT(selectMode()));
|
||||
toolbar->addAction(selectAct);
|
||||
|
||||
selectAct->setEnabled(true);
|
||||
drawAct->setEnabled(false);
|
||||
|
||||
toolbar->addSeparator();
|
||||
|
||||
QIcon cutIcon(":images/toolbar/cut.png");
|
||||
cutAct = new QAction(cutIcon, tr("Cut"), this);
|
||||
cutAct->setEnabled(true);
|
||||
toolbar->addAction(cutAct);
|
||||
connect(cutAct, SIGNAL(triggered()), workout, SLOT(cut()));
|
||||
|
||||
QIcon copyIcon(":images/toolbar/copy.png");
|
||||
copyAct = new QAction(copyIcon, tr("Copy"), this);
|
||||
copyAct->setEnabled(true);
|
||||
toolbar->addAction(copyAct);
|
||||
connect(copyAct, SIGNAL(triggered()), workout, SLOT(copy()));
|
||||
|
||||
QIcon pasteIcon(":images/toolbar/paste.png");
|
||||
pasteAct = new QAction(pasteIcon, tr("Paste"), this);
|
||||
pasteAct->setEnabled(false);
|
||||
toolbar->addAction(pasteAct);
|
||||
connect(pasteAct, SIGNAL(triggered()), workout, SLOT(paste()));
|
||||
|
||||
toolbar->addSeparator();
|
||||
|
||||
QIcon propertiesIcon(":images/toolbar/properties.png");
|
||||
propertiesAct = new QAction(propertiesIcon, tr("Properties"), this);
|
||||
connect(propertiesAct, SIGNAL(triggered()), this, SLOT(properties()));
|
||||
toolbar->addAction(propertiesAct);
|
||||
|
||||
QIcon zoomInIcon(":images/toolbar/zoom in.png");
|
||||
zoomInAct = new QAction(zoomInIcon, tr("Zoom In"), this);
|
||||
connect(zoomInAct, SIGNAL(triggered()), this, SLOT(zoomIn()));
|
||||
toolbar->addAction(zoomInAct);
|
||||
|
||||
QIcon zoomOutIcon(":images/toolbar/zoom out.png");
|
||||
zoomOutAct = new QAction(zoomOutIcon, tr("Zoom Out"), this);
|
||||
connect(zoomOutAct, SIGNAL(triggered()), this, SLOT(zoomOut()));
|
||||
toolbar->addAction(zoomOutAct);
|
||||
|
||||
// stretch the labels to the right hand side
|
||||
QWidget *empty = new QWidget(this);
|
||||
empty->setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Preferred);
|
||||
toolbar->addWidget(empty);
|
||||
|
||||
|
||||
xlabel = new QLabel("00:00");
|
||||
toolbar->addWidget(xlabel);
|
||||
|
||||
ylabel = new QLabel("150w");
|
||||
toolbar->addWidget(ylabel);
|
||||
|
||||
IFlabel = new QLabel("0 IF");
|
||||
toolbar->addWidget(IFlabel);
|
||||
|
||||
TSSlabel = new QLabel("0 TSS");
|
||||
toolbar->addWidget(TSSlabel);
|
||||
|
||||
#if 0 // not yet!
|
||||
// get updates..
|
||||
connect(context, SIGNAL(telemetryUpdate(RealtimeData)), this, SLOT(telemetryUpdate(RealtimeData)));
|
||||
telemetryUpdate(RealtimeData());
|
||||
#endif
|
||||
|
||||
// editing the code...
|
||||
code = new CodeEditor(this);
|
||||
code->setContextMenuPolicy(Qt::NoContextMenu); // no context menu
|
||||
code->installEventFilter(this); // filter the undo/redo stuff
|
||||
code->hide();
|
||||
|
||||
// WATTS and Duration for the cursor
|
||||
main->addWidget(toolbar);
|
||||
editor->addWidget(workout);
|
||||
editor->addWidget(scroll);
|
||||
layout->addLayout(editor);
|
||||
layout->addWidget(code);
|
||||
main->addLayout(layout);
|
||||
|
||||
// make it look right
|
||||
saveAct->setEnabled(false);
|
||||
undoAct->setEnabled(false);
|
||||
redoAct->setEnabled(false);
|
||||
|
||||
// watch for erg file selection
|
||||
connect(context, SIGNAL(ergFileSelected(ErgFile*)), this, SLOT(ergFileSelected(ErgFile*)));
|
||||
|
||||
// watch for erg run/stop
|
||||
connect(context, SIGNAL(start()), this, SLOT(start()));
|
||||
connect(context, SIGNAL(stop()), this, SLOT(stop()));
|
||||
|
||||
// text changed
|
||||
connect(code, SIGNAL(textChanged()), this, SLOT(qwkcodeChanged()));
|
||||
connect(code, SIGNAL(cursorPositionChanged()), workout, SLOT(hoverQwkcode()));
|
||||
|
||||
// scrollbar
|
||||
connect(scroll, SIGNAL(sliderMoved(int)), this, SLOT(scrollMoved()));
|
||||
|
||||
// set the widgets etc
|
||||
configChanged(CONFIG_APPEARANCE);
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::resizeEvent(QResizeEvent *)
|
||||
{
|
||||
// show or hide toolbar if big enough
|
||||
if (!recording && height() > MINTOOLHEIGHT) toolbar->show();
|
||||
else toolbar->hide();
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::configChanged(qint32)
|
||||
{
|
||||
setProperty("color", GColor(CTRAINPLOTBACKGROUND));
|
||||
QFontMetrics fm(workout->bigFont);
|
||||
xlabel->setFont(workout->bigFont);
|
||||
ylabel->setFont(workout->bigFont);
|
||||
IFlabel->setFont(workout->bigFont);
|
||||
TSSlabel->setFont(workout->bigFont);
|
||||
IFlabel->setFixedWidth(fm.boundingRect(" 0.85 IF ").width());
|
||||
TSSlabel->setFixedWidth(fm.boundingRect(" 100 TSS ").width());
|
||||
xlabel->setFixedWidth(fm.boundingRect(" 00:00:00 ").width());
|
||||
ylabel->setFixedWidth(fm.boundingRect(" 1000w ").width());
|
||||
|
||||
scroll->setStyleSheet(TabView::ourStyleSheet());
|
||||
toolbar->setStyleSheet(QString("::enabled { background: %1; color: %2; border: 0px; } ")
|
||||
.arg(GColor(CTRAINPLOTBACKGROUND).name())
|
||||
.arg(GCColor::invertColor(GColor(CTRAINPLOTBACKGROUND)).name()));
|
||||
|
||||
xlabel->setStyleSheet("color: darkGray;");
|
||||
ylabel->setStyleSheet("color: darkGray;");
|
||||
TSSlabel->setStyleSheet("color: darkGray;");
|
||||
IFlabel->setStyleSheet("color: darkGray;");
|
||||
|
||||
// maximum of 20 characters per line ?
|
||||
QFont f;
|
||||
QFontMetrics ff(f);
|
||||
code->setFixedWidth(ff.boundingRect("99x999s@999-999r999s@999-999").width()+20);
|
||||
|
||||
// text edit colors
|
||||
QPalette palette;
|
||||
palette.setColor(QPalette::Window, GColor(CTRAINPLOTBACKGROUND));
|
||||
palette.setColor(QPalette::Background, GColor(CTRAINPLOTBACKGROUND));
|
||||
|
||||
// only change base if moved away from white plots
|
||||
// which is a Mac thing
|
||||
#ifndef Q_OS_MAC
|
||||
if (GColor(CTRAINPLOTBACKGROUND) != Qt::white)
|
||||
#endif
|
||||
{
|
||||
//palette.setColor(QPalette::Base, GCColor::alternateColor(GColor(CTRAINPLOTBACKGROUND)));
|
||||
palette.setColor(QPalette::Base, GColor(CTRAINPLOTBACKGROUND));
|
||||
palette.setColor(QPalette::Window, GColor(CTRAINPLOTBACKGROUND));
|
||||
}
|
||||
|
||||
#ifndef Q_OS_MAC // the scrollers appear when needed on Mac, we'll keep that
|
||||
code->setStyleSheet(TabView::ourStyleSheet());
|
||||
#endif
|
||||
|
||||
palette.setColor(QPalette::WindowText, GCColor::invertColor(GColor(CTRAINPLOTBACKGROUND)));
|
||||
palette.setColor(QPalette::Text, GCColor::invertColor(GColor(CTRAINPLOTBACKGROUND)));
|
||||
code->setPalette(palette);
|
||||
repaint();
|
||||
}
|
||||
|
||||
bool
|
||||
WorkoutWindow::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
bool returning=false;
|
||||
|
||||
// we only filter out keyboard shortcuts for undo redo etc
|
||||
// in the qwkcode editor, anything else is of no interest.
|
||||
if (obj != code) return returning;
|
||||
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
|
||||
// we care about cmd / ctrl
|
||||
Qt::KeyboardModifiers kmod = static_cast<QInputEvent*>(event)->modifiers();
|
||||
bool ctrl = (kmod & Qt::ControlModifier) != 0;
|
||||
|
||||
switch(static_cast<QKeyEvent*>(event)->key()) {
|
||||
|
||||
case Qt::Key_Y:
|
||||
if (ctrl) {
|
||||
workout->redo();
|
||||
returning = true; // we grab all key events
|
||||
}
|
||||
break;
|
||||
|
||||
case Qt::Key_Z:
|
||||
if (ctrl) {
|
||||
workout->undo();
|
||||
returning=true;
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
return returning;
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::zoomIn()
|
||||
{
|
||||
setScroller(workout->zoomIn());
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::zoomOut()
|
||||
{
|
||||
setScroller(workout->zoomOut());
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::setScroller(QPointF v)
|
||||
{
|
||||
double minVX=v.x();
|
||||
double maxVX=v.y();
|
||||
|
||||
// do we even need it ?
|
||||
double vwidth = maxVX - minVX;
|
||||
if (vwidth >= workout->maxWX()) {
|
||||
// it needs to be hidden, the view fits
|
||||
scroll->hide();
|
||||
} else {
|
||||
// we need it, so set to right place
|
||||
scroll->setMinimum(0);
|
||||
scroll->setMaximum(workout->maxWX() - vwidth);
|
||||
scroll->setPageStep(vwidth);
|
||||
scroll->setValue(minVX);
|
||||
|
||||
// and show
|
||||
scroll->show();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::scrollMoved()
|
||||
{
|
||||
// is it visible!?
|
||||
if (!scroll->isVisible()) return;
|
||||
|
||||
// just move the view as needed
|
||||
double minVX = scroll->value();
|
||||
|
||||
// now set the view
|
||||
workout->setMinVX(minVX);
|
||||
workout->setMaxVX(minVX + scroll->pageStep());
|
||||
|
||||
// bleck
|
||||
workout->setBlockCursor();
|
||||
|
||||
// repaint
|
||||
workout->update();
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::ergFileSelected(ErgFile*f)
|
||||
{
|
||||
if (active) return;
|
||||
|
||||
if (workout->isDirty()) {
|
||||
QMessageBox msgBox;
|
||||
msgBox.setText(tr("You have unsaved changes to a workout."));
|
||||
msgBox.setInformativeText(tr("Do you want to save them?"));
|
||||
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
|
||||
msgBox.setDefaultButton(QMessageBox::Cancel);
|
||||
msgBox.setIcon(QMessageBox::Warning);
|
||||
msgBox.exec();
|
||||
|
||||
// save first, otherwise changes lost
|
||||
if(msgBox.clickedButton() == msgBox.button(QMessageBox::Yes)) {
|
||||
active = true;
|
||||
saveFile();
|
||||
active = false;
|
||||
}
|
||||
}
|
||||
|
||||
// just get on with it.
|
||||
ergFile = f;
|
||||
workout->ergFileSelected(f);
|
||||
|
||||
// almost certainly hides it on load
|
||||
setScroller(QPointF(workout->minVX(), workout->maxVX()));
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::newFile()
|
||||
{
|
||||
// new blank file clear points .. texts .. metadata etc
|
||||
ergFileSelected(NULL);
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::saveAs()
|
||||
{
|
||||
QString filename = QFileDialog::getSaveFileName(this, tr("Save Workout File"),
|
||||
appsettings->value(this, GC_WORKOUTDIR, "").toString(),
|
||||
"New Workout.erg (*.erg)");
|
||||
|
||||
// if they didn't select, give up.
|
||||
if (filename.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// New ergfile will be created almost empty
|
||||
ErgFile *newergFile = new ErgFile(context);
|
||||
|
||||
// we need to set sensible defaults for
|
||||
// all the metadata in the file.
|
||||
newergFile->Version = "2.0";
|
||||
newergFile->Units = "";
|
||||
newergFile->Filename = QFileInfo(filename).fileName();
|
||||
newergFile->filename = filename;
|
||||
newergFile->Name = "New Workout";
|
||||
newergFile->Ftp = newergFile->CP;
|
||||
newergFile->valid = true;
|
||||
newergFile->format = ERG; // default to couse until we know
|
||||
|
||||
// if we're save as from an existing keep all the data
|
||||
// EXCEPT filename, which has just been changed!
|
||||
if (ergFile) newergFile->setFrom(ergFile);
|
||||
|
||||
// Update with whatever is currently in editor
|
||||
workout->updateErgFile(newergFile);
|
||||
|
||||
// select new workout
|
||||
workout->ergFileSelected(newergFile);
|
||||
|
||||
// write file
|
||||
workout->save();
|
||||
|
||||
// add to collection with new name, a single new file
|
||||
Library::importFiles(context, QStringList(filename));
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::saveFile()
|
||||
{
|
||||
// if ergFile is null we need to save as, with the current
|
||||
// edit state being held by the workout editor
|
||||
// otherwise just write it to disk straight away as its
|
||||
// an existing ergFile we just need to write to
|
||||
if (ergFile) workout->save();
|
||||
else saveAs();
|
||||
|
||||
// force any other plots to take the changes
|
||||
context->notifyErgFileSelected(ergFile);
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::properties()
|
||||
{
|
||||
// metadata etc -- needs a dialog
|
||||
code->setHidden(!code->isHidden());
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::qwkcodeChanged()
|
||||
{
|
||||
workout->fromQwkcode(code->document()->toPlainText());
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::drawMode()
|
||||
{
|
||||
draw = true;
|
||||
drawAct->setEnabled(false);
|
||||
selectAct->setEnabled(true);
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::selectMode()
|
||||
{
|
||||
draw = false;
|
||||
drawAct->setEnabled(true);
|
||||
selectAct->setEnabled(false);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WorkoutWindow::start()
|
||||
{
|
||||
recording = true;
|
||||
scroll->hide();
|
||||
toolbar->hide();
|
||||
code->hide();
|
||||
workout->start();
|
||||
}
|
||||
|
||||
void
|
||||
WorkoutWindow::stop()
|
||||
{
|
||||
recording = false;
|
||||
if (height() > MINTOOLHEIGHT) toolbar->show();
|
||||
workout->stop();
|
||||
}
|
||||
Reference in New Issue
Block a user