/* * 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) : GcChartWindow(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; QHBoxLayout *layout = new QHBoxLayout; QVBoxLayout *editor = new QVBoxLayout; setChartLayout(main); 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 *dpiXFactor,18 *dpiYFactor)); 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 Intensity"); toolbar->addWidget(IFlabel); TSSlabel = new QLabel("0 Stress"); 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*dpiYFactor)) 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 Intensity ").width()); TSSlabel->setFixedWidth(fm.boundingRect(" 100 Stress ").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* dpiXFactor)); // 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(event)->modifiers(); bool ctrl = (kmod & Qt::ControlModifier) != 0; switch(static_cast(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(), "ERG workout (*.erg);;Zwift workout (*.zwo)"); // 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(); }