mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 16:18:42 +00:00
.. Import and Export perspectives to an '.gchartset' file as XML data. .. Added to MainWindow's View menu and the Manage Perspectives dialog.
2725 lines
89 KiB
C++
2725 lines
89 KiB
C++
/*
|
|
* Copyright (c) 2006 Sean C. Rhea (srhea@srhea.net)
|
|
* Copyright (c) 2013 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
|
|
*/
|
|
|
|
// QT
|
|
#include <QApplication>
|
|
#include <QtGui>
|
|
#include <QRegExp>
|
|
#include <QDesktopWidget>
|
|
#include <QNetworkProxyQuery>
|
|
#include <QMenuBar>
|
|
#include <QStyle>
|
|
#include <QTabBar>
|
|
#include <QStyleFactory>
|
|
#include <QRect>
|
|
|
|
// DATA STRUCTURES
|
|
#include "MainWindow.h"
|
|
#include "Context.h"
|
|
#include "Athlete.h"
|
|
#include "AthleteView.h"
|
|
#include "AthleteBackup.h"
|
|
|
|
#include "Colors.h"
|
|
#include "RideCache.h"
|
|
#include "RideItem.h"
|
|
#include "IntervalItem.h"
|
|
#include "RideFile.h"
|
|
#include "Settings.h"
|
|
#include "ErgDB.h"
|
|
#include "TodaysPlanWorkoutDownload.h"
|
|
#include "Library.h"
|
|
#include "LibraryParser.h"
|
|
#include "TrainDB.h"
|
|
#include "GcUpgrade.h"
|
|
#include "HelpWhatsThis.h"
|
|
#include "CsvRideFile.h"
|
|
|
|
// DIALOGS / DOWNLOADS / UPLOADS
|
|
#include "AboutDialog.h"
|
|
#include "ChooseCyclistDialog.h"
|
|
#include "ConfigDialog.h"
|
|
#include "DownloadRideDialog.h"
|
|
#include "ManualRideDialog.h"
|
|
#include "RideImportWizard.h"
|
|
#include "EstimateCPDialog.h"
|
|
#include "SolveCPDialog.h"
|
|
#include "ToolsRhoEstimator.h"
|
|
#include "VDOTCalculator.h"
|
|
#include "SplitActivityWizard.h"
|
|
#include "MergeActivityWizard.h"
|
|
#include "GenerateHeatMapDialog.h"
|
|
#include "BatchExportDialog.h"
|
|
#include "TodaysPlan.h"
|
|
#include "MeasuresDownload.h"
|
|
#include "WorkoutWizard.h"
|
|
#include "ErgDBDownloadDialog.h"
|
|
#include "AddDeviceWizard.h"
|
|
#include "Dropbox.h"
|
|
#include "GoogleDrive.h"
|
|
#include "KentUniversity.h"
|
|
#include "SixCycle.h"
|
|
#include "OpenData.h"
|
|
#include "AddCloudWizard.h"
|
|
#include "LocalFileStore.h"
|
|
#include "CloudService.h"
|
|
#ifdef GC_WANT_PYTHON
|
|
#include "FixPyScriptsDialog.h"
|
|
#endif
|
|
|
|
// GUI Widgets
|
|
#include "Tab.h"
|
|
#include "GcToolBar.h"
|
|
#include "NewSideBar.h"
|
|
#include "HelpWindow.h"
|
|
#include "Perspective.h"
|
|
#include "PerspectiveDialog.h"
|
|
|
|
#if !defined(Q_OS_MAC)
|
|
#include "QTFullScreen.h" // not mac!
|
|
#endif
|
|
#include "../qtsolutions/segmentcontrol/qtsegmentcontrol.h"
|
|
|
|
// SEARCH / FILTER
|
|
#include "NamedSearch.h"
|
|
#include "SearchFilterBox.h"
|
|
|
|
// LTM CHART DRAG/DROP PARSE
|
|
#include "LTMChartParser.h"
|
|
|
|
// CloudDB
|
|
#ifdef GC_HAS_CLOUD_DB
|
|
#include "CloudDBCommon.h"
|
|
#include "CloudDBChart.h"
|
|
#include "CloudDBUserMetric.h"
|
|
#include "CloudDBCurator.h"
|
|
#include "CloudDBStatus.h"
|
|
#include "CloudDBTelemetry.h"
|
|
#include "CloudDBVersion.h"
|
|
#include "GcUpgrade.h"
|
|
#endif
|
|
#include "Secrets.h"
|
|
|
|
#if defined(_MSC_VER) && defined(_WIN64)
|
|
#include "WindowsCrashHandler.cpp"
|
|
#endif
|
|
|
|
|
|
// We keep track of all theopen mainwindows
|
|
QList<MainWindow *> mainwindows;
|
|
extern QDesktopWidget *desktop;
|
|
extern ConfigDialog *configdialog_ptr;
|
|
extern QString gl_version;
|
|
extern double gl_major; // 1.x 2.x 3.x - we insist on 2.x or higher to enable OpenGL
|
|
|
|
MainWindow::MainWindow(const QDir &home)
|
|
{
|
|
/*----------------------------------------------------------------------
|
|
* Bootstrap
|
|
*--------------------------------------------------------------------*/
|
|
setAttribute(Qt::WA_DeleteOnClose);
|
|
mainwindows.append(this); // add us to the list of open windows
|
|
pactive = init = false;
|
|
|
|
// create a splash to keep user informed on first load
|
|
// first one in middle of display, not middle of window
|
|
setSplash(true);
|
|
|
|
#if defined(_MSC_VER) && defined(_WIN64)
|
|
// set dbg/stacktrace directory for Windows to the athlete directory
|
|
// don't use the GC_HOMEDIR .ini value, since we want to have a proper path
|
|
// even if default athlete dirs are used.
|
|
QDir varHome = home;
|
|
varHome.cdUp();
|
|
setCrashFilePath(varHome.canonicalPath().toStdString());
|
|
|
|
#endif
|
|
|
|
// bootstrap
|
|
Context *context = new Context(this);
|
|
context->athlete = new Athlete(context, home);
|
|
currentTab = new Tab(context);
|
|
|
|
// get rid of splash when currentTab is shown
|
|
clearSplash();
|
|
|
|
setWindowIcon(QIcon(":images/gc.png"));
|
|
setWindowTitle(context->athlete->home->root().dirName());
|
|
setContentsMargins(0,0,0,0);
|
|
setAcceptDrops(true);
|
|
|
|
Library::initialise(context->athlete->home->root());
|
|
QNetworkProxyQuery npq(QUrl("http://www.google.com"));
|
|
QList<QNetworkProxy> listOfProxies = QNetworkProxyFactory::systemProxyForQuery(npq);
|
|
if (listOfProxies.count() > 0) {
|
|
QNetworkProxy::setApplicationProxy(listOfProxies.first());
|
|
}
|
|
|
|
static const QIcon hideIcon(":images/toolbar/main/hideside.png");
|
|
static const QIcon rhideIcon(":images/toolbar/main/hiderside.png");
|
|
static const QIcon showIcon(":images/toolbar/main/showside.png");
|
|
static const QIcon rshowIcon(":images/toolbar/main/showrside.png");
|
|
static const QIcon tabIcon(":images/toolbar/main/tab.png");
|
|
static const QIcon tileIcon(":images/toolbar/main/tile.png");
|
|
static const QIcon fullIcon(":images/toolbar/main/togglefull.png");
|
|
|
|
#ifndef Q_OS_MAC
|
|
fullScreen = new QTFullScreen(this);
|
|
#endif
|
|
|
|
// if no workout directory is configured, default to the
|
|
// top level GoldenCheetah directory
|
|
if (appsettings->value(NULL, GC_WORKOUTDIR).toString() == "")
|
|
appsettings->setValue(GC_WORKOUTDIR, QFileInfo(context->athlete->home->root().canonicalPath() + "/../").canonicalPath());
|
|
|
|
/*----------------------------------------------------------------------
|
|
* GUI setup
|
|
*--------------------------------------------------------------------*/
|
|
if (appsettings->contains(GC_SETTINGS_MAIN_GEOM)) {
|
|
restoreGeometry(appsettings->value(this, GC_SETTINGS_MAIN_GEOM).toByteArray());
|
|
restoreState(appsettings->value(this, GC_SETTINGS_MAIN_STATE).toByteArray());
|
|
} else {
|
|
// first run -- lets set some sensible defaults...
|
|
// lets put it in the middle of screen 1
|
|
QRect screenSize = desktop->availableGeometry();
|
|
struct SizeSettings app = GCColor::defaultSizes(screenSize.height(), screenSize.width());
|
|
|
|
// center on the available screen (minus toolbar/sidebar)
|
|
move((screenSize.width()-screenSize.x())/2 - app.width/2,
|
|
(screenSize.height()-screenSize.y())/2 - app.height/2);
|
|
|
|
// set to the right default
|
|
resize(app.width, app.height);
|
|
|
|
// set all the default font sizes
|
|
appsettings->setValue(GC_FONT_DEFAULT_SIZE, app.defaultFont);
|
|
appsettings->setValue(GC_FONT_CHARTLABELS_SIZE, app.labelFont);
|
|
|
|
}
|
|
|
|
// store "last_openend" athlete for next time
|
|
appsettings->setValue(GC_SETTINGS_LAST, context->athlete->home->root().dirName());
|
|
|
|
/*----------------------------------------------------------------------
|
|
* ScopeBar as sidebar from v3.6
|
|
*--------------------------------------------------------------------*/
|
|
|
|
sidebar = new NewSideBar(context, this);
|
|
HelpWhatsThis *helpNewSideBar = new HelpWhatsThis(sidebar);
|
|
sidebar->setWhatsThis(helpNewSideBar->getWhatsThisText(HelpWhatsThis::ScopeBar));
|
|
|
|
sidebar->addItem(QImage(":sidebar/athlete.png"), tr("athletes"), 0, helpNewSideBar->getWhatsThisText(HelpWhatsThis::ScopeBar_Athletes));
|
|
sidebar->setItemEnabled(1, false);
|
|
|
|
sidebar->addItem(QImage(":sidebar/plan.png"), tr("plan"), 1), tr("Feature not implemented yet");
|
|
sidebar->setItemEnabled(1, false);
|
|
|
|
sidebar->addItem(QImage(":sidebar/trends.png"), tr("trends"), 2, helpNewSideBar->getWhatsThisText(HelpWhatsThis::ScopeBar_Trends));
|
|
sidebar->addItem(QImage(":sidebar/assess.png"), tr("activities"), 3, helpNewSideBar->getWhatsThisText(HelpWhatsThis::ScopeBar_Rides));
|
|
sidebar->setItemSelected(3, true);
|
|
|
|
sidebar->addItem(QImage(":sidebar/reflect.png"), tr("reflect"), 4), tr("Feature not implemented yet");
|
|
sidebar->setItemEnabled(4, false);
|
|
|
|
sidebar->addItem(QImage(":sidebar/train.png"), tr("train"), 5, helpNewSideBar->getWhatsThisText(HelpWhatsThis::ScopeBar_Train));
|
|
|
|
sidebar->addStretch();
|
|
sidebar->addItem(QImage(":sidebar/apps.png"), tr("apps"), 6, tr("Feature not implemented yet"));
|
|
sidebar->setItemEnabled(6, false);
|
|
sidebar->addStretch();
|
|
|
|
// we can click on the quick icons, but they aren't selectable views
|
|
sidebar->addItem(QImage(":sidebar/sync.png"), tr("sync"), 7, helpNewSideBar->getWhatsThisText(HelpWhatsThis::ScopeBar_Sync));
|
|
sidebar->setItemSelectable(7, false);
|
|
sidebar->addItem(QImage(":sidebar/prefs.png"), tr("options"), 8, helpNewSideBar->getWhatsThisText(HelpWhatsThis::ScopeBar_Options));
|
|
sidebar->setItemSelectable(8, false);
|
|
|
|
connect(sidebar, SIGNAL(itemClicked(int)), this, SLOT(sidebarClicked(int)));
|
|
connect(sidebar, SIGNAL(itemSelected(int)), this, SLOT(sidebarSelected(int)));
|
|
|
|
/*----------------------------------------------------------------------
|
|
* What's this Context Help
|
|
*--------------------------------------------------------------------*/
|
|
|
|
// Help for the whole window
|
|
HelpWhatsThis *help = new HelpWhatsThis(this);
|
|
this->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::Default));
|
|
// add Help Button
|
|
QAction *myHelper = QWhatsThis::createAction (this);
|
|
this->addAction(myHelper);
|
|
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Toolbar
|
|
*--------------------------------------------------------------------*/
|
|
head = new GcToolBar(this);
|
|
|
|
QStyle *toolStyle = QStyleFactory::create("fusion");
|
|
QPalette metal;
|
|
metal.setColor(QPalette::Button, QColor(215,215,215));
|
|
|
|
// get those icons
|
|
sidebarIcon = iconFromPNG(":images/mac/sidebar.png", QSize(16*dpiXFactor,16*dpiXFactor));
|
|
lowbarIcon = iconFromPNG(":images/mac/lowbar.png", QSize(16*dpiXFactor,16*dpiXFactor));
|
|
tabbedIcon = iconFromPNG(":images/mac/tabbed.png", QSize(20*dpiXFactor,20*dpiXFactor));
|
|
tiledIcon = iconFromPNG(":images/mac/tiled.png", QSize(20*dpiXFactor,20*dpiXFactor));
|
|
backIcon = iconFromPNG(":images/mac/back.png");
|
|
forwardIcon = iconFromPNG(":images/mac/forward.png");
|
|
QSize isize(20 *dpiXFactor,20 *dpiYFactor);
|
|
|
|
back = new QPushButton(this);
|
|
back->setIcon(backIcon);
|
|
back->setFixedHeight(24 *dpiYFactor);
|
|
back->setFixedWidth(24 *dpiYFactor);
|
|
back->setIconSize(isize);
|
|
back->setStyle(toolStyle);
|
|
connect(back, SIGNAL(clicked(bool)), this, SIGNAL(backClicked()));
|
|
|
|
forward = new QPushButton(this);
|
|
forward->setIcon(forwardIcon);
|
|
forward->setFixedHeight(24 *dpiYFactor);
|
|
forward->setFixedWidth(24 *dpiYFactor);
|
|
forward->setIconSize(isize);
|
|
forward->setStyle(toolStyle);
|
|
connect(forward, SIGNAL(clicked(bool)), this, SIGNAL(forwardClicked()));
|
|
|
|
lowbar = new QPushButton(this);
|
|
lowbar->setIcon(lowbarIcon);
|
|
lowbar->setFixedHeight(24 *dpiYFactor);
|
|
lowbar->setIconSize(isize);
|
|
lowbar->setStyle(toolStyle);
|
|
lowbar->setToolTip(tr("Toggle Compare Pane"));
|
|
lowbar->setPalette(metal);
|
|
connect(lowbar, SIGNAL(clicked(bool)), this, SLOT(toggleLowbar()));
|
|
HelpWhatsThis *helpLowBar = new HelpWhatsThis(lowbar);
|
|
lowbar->setWhatsThis(helpLowBar->getWhatsThisText(HelpWhatsThis::ToolBar_ToggleComparePane));
|
|
|
|
sidelist = new QPushButton(this);
|
|
sidelist->setIcon(sidebarIcon);
|
|
sidelist->setFixedHeight(24 * dpiYFactor);
|
|
sidelist->setIconSize(isize);
|
|
sidelist->setStyle(toolStyle);
|
|
sidelist->setToolTip(tr("Toggle Sidebar"));
|
|
sidelist->setPalette(metal);
|
|
connect(sidelist, SIGNAL(clicked(bool)), this, SLOT(toggleSidebar()));
|
|
HelpWhatsThis *helpSideBar = new HelpWhatsThis(sidelist);
|
|
sidelist->setWhatsThis(helpSideBar->getWhatsThisText(HelpWhatsThis::ToolBar_ToggleSidebar));
|
|
|
|
styleSelector = new QtSegmentControl(this);
|
|
styleSelector->setStyle(toolStyle);
|
|
styleSelector->setCount(2);
|
|
styleSelector->setSegmentIcon(0, tabbedIcon);
|
|
styleSelector->setSegmentIcon(1, tiledIcon);
|
|
styleSelector->setSegmentToolTip(0, tr("Tabbed View"));
|
|
styleSelector->setSegmentToolTip(1, tr("Tiled View"));
|
|
styleSelector->setSelectionBehavior(QtSegmentControl::SelectOne); //wince. spelling. ugh
|
|
styleSelector->setFixedHeight(24 * dpiYFactor);
|
|
styleSelector->setIconSize(isize);
|
|
styleSelector->setPalette(metal);
|
|
connect(styleSelector, SIGNAL(segmentSelected(int)), this, SLOT(setStyleFromSegment(int))); //avoid toggle infinitely
|
|
|
|
// What's this button
|
|
whatsthis = new QPushButton(this);
|
|
whatsthis->setIcon(myHelper->icon());
|
|
whatsthis->setFixedHeight(24 * dpiYFactor);
|
|
whatsthis->setIconSize(isize);
|
|
whatsthis->setStyle(toolStyle);
|
|
whatsthis->setToolTip(tr("What's This?"));
|
|
whatsthis->setPalette(metal);
|
|
connect(whatsthis, SIGNAL(clicked(bool)), this, SLOT(enterWhatsThisMode()));
|
|
|
|
#if defined(WIN32) || defined (Q_OS_LINUX)
|
|
// are we in hidpi mode? if so undo global defaults for toolbar pushbuttons
|
|
if (dpiXFactor > 1) {
|
|
QString nopad = QString("QPushButton { padding-left: 0px; padding-right: 0px; "
|
|
" padding-top: 0px; padding-bottom: 0px; }");
|
|
sidelist->setStyleSheet(nopad);
|
|
lowbar->setStyleSheet(nopad);
|
|
whatsthis->setStyleSheet(nopad);
|
|
}
|
|
#endif
|
|
|
|
// add a search box on far right, but with a little space too
|
|
perspectiveSelector = new QComboBox(this);
|
|
perspectiveSelector->setStyle(toolStyle);
|
|
perspectiveSelector->setFixedWidth(200 * dpiXFactor);
|
|
perspectiveSelector->setFixedHeight(28 * dpiYFactor);
|
|
connect(perspectiveSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(perspectiveSelected(int)));
|
|
|
|
searchBox = new SearchFilterBox(this,context,false);
|
|
|
|
searchBox->setStyle(toolStyle);
|
|
searchBox->setFixedWidth(400 * dpiXFactor);
|
|
searchBox->setFixedHeight(28 * dpiYFactor);
|
|
|
|
QWidget *space = new QWidget(this);
|
|
space->setAutoFillBackground(false);
|
|
space->setFixedWidth(5 * dpiYFactor);
|
|
|
|
head->addWidget(space);
|
|
head->addWidget(back);
|
|
head->addWidget(forward);
|
|
head->addWidget(perspectiveSelector);
|
|
head->addStretch();
|
|
head->addWidget(sidelist);
|
|
head->addWidget(lowbar);
|
|
head->addWidget(styleSelector);
|
|
head->addWidget(whatsthis);
|
|
head->setFixedHeight(searchBox->height() + (16 *dpiXFactor));
|
|
|
|
connect(searchBox, SIGNAL(searchResults(QStringList)), this, SLOT(setFilter(QStringList)));
|
|
connect(searchBox, SIGNAL(searchClear()), this, SLOT(clearFilter()));
|
|
HelpWhatsThis *helpSearchBox = new HelpWhatsThis(searchBox);
|
|
searchBox->setWhatsThis(helpSearchBox->getWhatsThisText(HelpWhatsThis::SearchFilterBox));
|
|
|
|
space = new Spacer(this);
|
|
space->setFixedWidth(5 *dpiYFactor);
|
|
head->addWidget(space);
|
|
head->addWidget(searchBox);
|
|
space = new Spacer(this);
|
|
space->setFixedWidth(5 *dpiYFactor);
|
|
head->addWidget(space);
|
|
|
|
#ifdef Q_OS_LINUX
|
|
// check opengl is available with version 2 or higher
|
|
// only do this on Linux since Windows and MacOS have opengl "issues"
|
|
QOffscreenSurface surf;
|
|
surf.create();
|
|
|
|
QOpenGLContext ctx;
|
|
ctx.create();
|
|
ctx.makeCurrent(&surf);
|
|
|
|
// OpenGL version number
|
|
gl_version = QString::fromUtf8((char *)(ctx.functions()->glGetString(GL_VERSION)));
|
|
gl_major = Utils::number(gl_version);
|
|
#endif
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Central Widget
|
|
*--------------------------------------------------------------------*/
|
|
|
|
tabbar = new DragBar(this);
|
|
tabbar->setTabsClosable(false); // use athlete view
|
|
#ifdef Q_OS_MAC
|
|
tabbar->setDocumentMode(true);
|
|
#endif
|
|
|
|
athleteView = new AthleteView(context);
|
|
viewStack = new QStackedWidget(this);
|
|
viewStack->addWidget(athleteView);
|
|
|
|
tabStack = new QStackedWidget(this);
|
|
viewStack->addWidget(tabStack);
|
|
|
|
// first tab
|
|
tabs.insert(currentTab->context->athlete->home->root().dirName(), currentTab);
|
|
|
|
// stack, list and bar all share a common index
|
|
tabList.append(currentTab);
|
|
tabbar->addTab(currentTab->context->athlete->home->root().dirName());
|
|
tabStack->addWidget(currentTab);
|
|
tabStack->setCurrentIndex(0);
|
|
|
|
connect(tabbar, SIGNAL(dragTab(int)), this, SLOT(switchTab(int)));
|
|
connect(tabbar, SIGNAL(currentChanged(int)), this, SLOT(switchTab(int)));
|
|
//connect(tabbar, SIGNAL(tabCloseRequested(int)), this, SLOT(closeTabClicked(int))); // use athlete view
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Central Widget
|
|
*--------------------------------------------------------------------*/
|
|
|
|
QWidget *central = new QWidget(this);
|
|
setContentsMargins(0,0,0,0);
|
|
central->setContentsMargins(0,0,0,0);
|
|
QVBoxLayout *mainLayout = new QVBoxLayout(central);
|
|
mainLayout->setSpacing(0);
|
|
mainLayout->setContentsMargins(0,0,0,0);
|
|
mainLayout->addWidget(head);
|
|
QHBoxLayout *lrlayout = new QHBoxLayout();
|
|
mainLayout->addLayout(lrlayout);
|
|
lrlayout->setSpacing(0);
|
|
lrlayout->setContentsMargins(0,0,0,0);
|
|
lrlayout->addWidget(sidebar);
|
|
QVBoxLayout *tablayout = new QVBoxLayout();
|
|
tablayout->setSpacing(0);
|
|
tablayout->setContentsMargins(0,0,0,0);
|
|
lrlayout->addLayout(tablayout);
|
|
tablayout->addWidget(tabbar);
|
|
tablayout->addWidget(viewStack);
|
|
setCentralWidget(central);
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Application Menus
|
|
*--------------------------------------------------------------------*/
|
|
#ifdef WIN32
|
|
QString menuColorString = (GCColor::isFlat() ? GColor(CCHROME).name() : "rgba(225,225,225)");
|
|
menuBar()->setStyleSheet(QString("QMenuBar { color: black; background: %1; }"
|
|
"QMenuBar::item { color: black; background: %1; }").arg(menuColorString));
|
|
menuBar()->setContentsMargins(0,0,0,0);
|
|
#endif
|
|
|
|
// ATHLETE (FILE) MENU
|
|
QMenu *fileMenu = menuBar()->addMenu(tr("&Athlete"));
|
|
|
|
openTabMenu = fileMenu->addMenu(tr("Open..."));
|
|
connect(openTabMenu, SIGNAL(aboutToShow()), this, SLOT(setOpenTabMenu()));
|
|
|
|
tabMapper = new QSignalMapper(this); // maps each option
|
|
connect(tabMapper, SIGNAL(mapped(const QString &)), this, SLOT(openTab(const QString &)));
|
|
|
|
fileMenu->addSeparator();
|
|
backupAthleteMenu = fileMenu->addMenu(tr("Backup..."));
|
|
connect(backupAthleteMenu, SIGNAL(aboutToShow()), this, SLOT(setBackupAthleteMenu()));
|
|
backupMapper = new QSignalMapper(this); // maps each option
|
|
connect(backupMapper, SIGNAL(mapped(const QString &)), this, SLOT(backupAthlete(const QString &)));
|
|
|
|
fileMenu->addSeparator();
|
|
deleteAthleteMenu = fileMenu->addMenu(tr("Delete..."));
|
|
connect(deleteAthleteMenu, SIGNAL(aboutToShow()), this, SLOT(setDeleteAthleteMenu()));
|
|
deleteMapper = new QSignalMapper(this); // maps each option
|
|
connect(deleteMapper, SIGNAL(mapped(const QString &)), this, SLOT(deleteAthlete(const QString &)));
|
|
|
|
fileMenu->addSeparator();
|
|
fileMenu->addAction(tr("Save all modified activities"), this, SLOT(saveAllUnsavedRides()));
|
|
fileMenu->addSeparator();
|
|
fileMenu->addAction(tr("Close Window"), this, SLOT(closeWindow()));
|
|
//fileMenu->addAction(tr("&Close Tab"), this, SLOT(closeTab())); use athlete view
|
|
|
|
HelpWhatsThis *fileMenuHelp = new HelpWhatsThis(fileMenu);
|
|
fileMenu->setWhatsThis(fileMenuHelp->getWhatsThisText(HelpWhatsThis::MenuBar_Athlete));
|
|
|
|
// ACTIVITY MENU
|
|
QMenu *rideMenu = menuBar()->addMenu(tr("A&ctivity"));
|
|
rideMenu->addAction(tr("&Download from device..."), this, SLOT(downloadRide()), tr("Ctrl+D"));
|
|
rideMenu->addAction(tr("&Import from file..."), this, SLOT (importFile()), tr ("Ctrl+I"));
|
|
rideMenu->addAction(tr("&Manual entry..."), this, SLOT(manualRide()), tr("Ctrl+M"));
|
|
rideMenu->addSeparator ();
|
|
rideMenu->addAction(tr("&Export..."), this, SLOT(exportRide()), tr("Ctrl+E"));
|
|
rideMenu->addAction(tr("&Batch export..."), this, SLOT(exportBatch()), tr("Ctrl+B"));
|
|
|
|
rideMenu->addSeparator ();
|
|
rideMenu->addAction(tr("&Save activity"), this, SLOT(saveRide()), tr("Ctrl+S"));
|
|
rideMenu->addAction(tr("D&elete activity..."), this, SLOT(deleteRide()));
|
|
rideMenu->addAction(tr("Split &activity..."), this, SLOT(splitRide()));
|
|
rideMenu->addAction(tr("Combine activities..."), this, SLOT(mergeRide()));
|
|
rideMenu->addSeparator ();
|
|
rideMenu->addAction(tr("Find intervals..."), this, SLOT(addIntervals()), tr (""));
|
|
|
|
HelpWhatsThis *helpRideMenu = new HelpWhatsThis(rideMenu);
|
|
rideMenu->setWhatsThis(helpRideMenu->getWhatsThisText(HelpWhatsThis::MenuBar_Activity));
|
|
|
|
// SHARE MENU
|
|
QMenu *shareMenu = menuBar()->addMenu(tr("Sha&re"));
|
|
|
|
// default options
|
|
shareAction = new QAction(tr("Add Cloud Account..."), this);
|
|
shareAction->setShortcut(tr("Ctrl+A"));
|
|
connect(shareAction, SIGNAL(triggered(bool)), this, SLOT(addAccount()));
|
|
shareMenu->addAction(shareAction);
|
|
shareMenu->addSeparator();
|
|
|
|
uploadMenu = shareMenu->addMenu(tr("Upload Activity..."));
|
|
syncMenu = shareMenu->addMenu(tr("Synchronise Activities..."));
|
|
measuresMenu = shareMenu->addMenu(tr("Get Measures..."));
|
|
shareMenu->addSeparator();
|
|
checkAction = new QAction(tr("Check For New Activities"), this);
|
|
checkAction->setShortcut(tr("Ctrl-C"));
|
|
connect(checkAction, SIGNAL(triggered(bool)), this, SLOT(checkCloud()));
|
|
shareMenu->addAction(checkAction);
|
|
|
|
|
|
// set the menus to reflect the configured accounts
|
|
connect(uploadMenu, SIGNAL(aboutToShow()), this, SLOT(setUploadMenu()));
|
|
connect(syncMenu, SIGNAL(aboutToShow()), this, SLOT(setSyncMenu()));
|
|
|
|
connect(uploadMenu, SIGNAL(triggered(QAction*)), this, SLOT(uploadCloud(QAction*)));
|
|
connect(syncMenu, SIGNAL(triggered(QAction*)), this, SLOT(syncCloud(QAction*)));
|
|
connect(measuresMenu, SIGNAL(aboutToShow()), this, SLOT(setMeasuresMenu()));
|
|
connect(measuresMenu, SIGNAL(triggered(QAction*)), this, SLOT(downloadMeasures(QAction*)));
|
|
|
|
HelpWhatsThis *helpShare = new HelpWhatsThis(shareMenu);
|
|
shareMenu->setWhatsThis(helpShare->getWhatsThisText(HelpWhatsThis::MenuBar_Share));
|
|
|
|
// TOOLS MENU
|
|
QMenu *optionsMenu = menuBar()->addMenu(tr("&Tools"));
|
|
optionsMenu->addAction(tr("CP and W' Estimator..."), this, SLOT(showEstimateCP()));
|
|
optionsMenu->addAction(tr("CP and W' Solver..."), this, SLOT(showSolveCP()));
|
|
optionsMenu->addAction(tr("Air Density (Rho) Estimator..."), this, SLOT(showRhoEstimator()));
|
|
optionsMenu->addAction(tr("VDOT and T-Pace Calculator..."), this, SLOT(showVDOTCalculator()));
|
|
|
|
optionsMenu->addSeparator();
|
|
optionsMenu->addAction(tr("Create a new workout..."), this, SLOT(showWorkoutWizard()));
|
|
optionsMenu->addAction(tr("Download workouts from ErgDB..."), this, SLOT(downloadErgDB()));
|
|
optionsMenu->addAction(tr("Download workouts from Today's Plan..."), this, SLOT(downloadTodaysPlanWorkouts()));
|
|
optionsMenu->addAction(tr("Import workouts, videos, videoSyncs..."), this, SLOT(importWorkout()));
|
|
optionsMenu->addAction(tr("Scan disk for workouts, videos, videoSyncs..."), this, SLOT(manageLibrary()));
|
|
|
|
optionsMenu->addAction(tr("Create Heat Map..."), this, SLOT(generateHeatMap()), tr(""));
|
|
optionsMenu->addAction(tr("Export Metrics as CSV..."), this, SLOT(exportMetrics()), tr(""));
|
|
|
|
#ifdef GC_HAS_CLOUD_DB
|
|
// CloudDB options
|
|
optionsMenu->addSeparator();
|
|
optionsMenu->addAction(tr("Cloud Status..."), this, SLOT(cloudDBshowStatus()));
|
|
|
|
QMenu *cloudDBMenu = optionsMenu->addMenu(tr("Cloud Contributions"));
|
|
cloudDBMenu->addAction(tr("Maintain charts"), this, SLOT(cloudDBuserEditChart()));
|
|
cloudDBMenu->addAction(tr("Maintain user metrics"), this, SLOT(cloudDBuserEditUserMetric()));
|
|
|
|
if (CloudDBCommon::addCuratorFeatures) {
|
|
QMenu *cloudDBCurator = optionsMenu->addMenu(tr("Cloud Curator"));
|
|
cloudDBCurator->addAction(tr("Curate charts"), this, SLOT(cloudDBcuratorEditChart()));
|
|
cloudDBCurator->addAction(tr("Curate user metrics"), this, SLOT(cloudDBcuratorEditUserMetric()));
|
|
}
|
|
|
|
#endif
|
|
#ifndef Q_OS_MAC
|
|
optionsMenu->addSeparator();
|
|
#endif
|
|
// options are always at the end of the tools menu
|
|
QAction *pref = optionsMenu->addAction(tr("&Options..."), this, SLOT(showOptions()));
|
|
pref->setMenuRole(QAction:: PreferencesRole);
|
|
|
|
HelpWhatsThis *optionsMenuHelp = new HelpWhatsThis(optionsMenu);
|
|
optionsMenu->setWhatsThis(optionsMenuHelp->getWhatsThisText(HelpWhatsThis::MenuBar_Tools));
|
|
|
|
|
|
QMenu *editMenu = menuBar()->addMenu(tr("&Edit"));
|
|
// Add all the data processors to the tools menu
|
|
const DataProcessorFactory &factory = DataProcessorFactory::instance();
|
|
QMap<QString, DataProcessor*> processors = factory.getProcessors(true);
|
|
|
|
if (processors.count()) {
|
|
|
|
toolMapper = new QSignalMapper(this); // maps each option
|
|
QMapIterator<QString, DataProcessor*> i(processors);
|
|
connect(toolMapper, SIGNAL(mapped(const QString &)), this, SLOT(manualProcess(const QString &)));
|
|
|
|
i.toFront();
|
|
while (i.hasNext()) {
|
|
i.next();
|
|
// The localized processor name is shown in menu
|
|
QAction *action = new QAction(QString("%1...").arg(i.value()->name()), this);
|
|
editMenu->addAction(action);
|
|
connect(action, SIGNAL(triggered()), toolMapper, SLOT(map()));
|
|
toolMapper->setMapping(action, i.key());
|
|
}
|
|
}
|
|
|
|
#ifdef GC_WANT_PYTHON
|
|
// add custom python fix entry to edit menu
|
|
pyFixesMenu = editMenu->addMenu(tr("Python fixes"));
|
|
connect(editMenu, SIGNAL(aboutToShow()), this, SLOT(onEditMenuAboutToShow()));
|
|
connect(pyFixesMenu, SIGNAL(aboutToShow()), this, SLOT(buildPyFixesMenu()));
|
|
#endif
|
|
|
|
HelpWhatsThis *editMenuHelp = new HelpWhatsThis(editMenu);
|
|
editMenu->setWhatsThis(editMenuHelp->getWhatsThisText(HelpWhatsThis::MenuBar_Edit));
|
|
|
|
// VIEW MENU
|
|
QMenu *viewMenu = menuBar()->addMenu(tr("&View"));
|
|
#ifndef Q_OS_MAC
|
|
viewMenu->addAction(tr("Toggle Full Screen"), this, SLOT(toggleFullScreen()), QKeySequence("F11"));
|
|
#else
|
|
viewMenu->addAction(tr("Toggle Full Screen"), this, SLOT(toggleFullScreen()));
|
|
#endif
|
|
showhideSidebar = viewMenu->addAction(tr("Show Left Sidebar"), this, SLOT(showSidebar(bool)));
|
|
showhideSidebar->setCheckable(true);
|
|
showhideSidebar->setChecked(true);
|
|
showhideLowbar = viewMenu->addAction(tr("Show Compare Pane"), this, SLOT(showLowbar(bool)));
|
|
showhideLowbar->setCheckable(true);
|
|
showhideLowbar->setChecked(false);
|
|
showhideToolbar = viewMenu->addAction(tr("Show Toolbar"), this, SLOT(showToolbar(bool)));
|
|
showhideToolbar->setCheckable(true);
|
|
showhideToolbar->setChecked(true);
|
|
showhideTabbar = viewMenu->addAction(tr("Show Athlete Tabs"), this, SLOT(showTabbar(bool)));
|
|
showhideTabbar->setCheckable(true);
|
|
showhideTabbar->setChecked(true);
|
|
|
|
viewMenu->addSeparator();
|
|
viewMenu->addAction(tr("Activities"), this, SLOT(selectAnalysis()));
|
|
viewMenu->addAction(tr("Trends"), this, SLOT(selectHome()));
|
|
viewMenu->addAction(tr("Train"), this, SLOT(selectTrain()));
|
|
viewMenu->addSeparator();
|
|
viewMenu->addAction(tr("Import Perspective..."), this, SLOT(importPerspective()));
|
|
viewMenu->addAction(tr("Export Perspective..."), this, SLOT(exportPerspective()));
|
|
viewMenu->addSeparator();
|
|
subChartMenu = viewMenu->addMenu(tr("Add Chart"));
|
|
viewMenu->addAction(tr("Import Chart..."), this, SLOT(importChart()));
|
|
#ifdef GC_HAS_CLOUD_DB
|
|
viewMenu->addAction(tr("Upload Chart..."), this, SLOT(exportChartToCloudDB()));
|
|
viewMenu->addAction(tr("Download Chart..."), this, SLOT(addChartFromCloudDB()));
|
|
viewMenu->addSeparator();
|
|
#endif
|
|
viewMenu->addAction(tr("Reset Layout"), this, SLOT(resetWindowLayout()));
|
|
styleAction = viewMenu->addAction(tr("Tabbed not Tiled"), this, SLOT(toggleStyle()));
|
|
styleAction->setCheckable(true);
|
|
styleAction->setChecked(true);
|
|
|
|
|
|
connect(subChartMenu, SIGNAL(aboutToShow()), this, SLOT(setSubChartMenu()));
|
|
connect(subChartMenu, SIGNAL(triggered(QAction*)), this, SLOT(addChart(QAction*)));
|
|
|
|
HelpWhatsThis *viewMenuHelp = new HelpWhatsThis(viewMenu);
|
|
viewMenu->setWhatsThis(viewMenuHelp->getWhatsThisText(HelpWhatsThis::MenuBar_View));
|
|
|
|
// HELP MENU
|
|
QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
|
|
helpMenu->addAction(tr("&Help Overview"), this, SLOT(helpWindow()));
|
|
helpMenu->addAction(myHelper);
|
|
helpMenu->addSeparator();
|
|
helpMenu->addAction(tr("&User Guide"), this, SLOT(helpView()));
|
|
helpMenu->addAction(tr("&Log a bug or feature request"), this, SLOT(logBug()));
|
|
helpMenu->addAction(tr("&Discussion and Support Forum"), this, SLOT(support()));
|
|
helpMenu->addSeparator();
|
|
helpMenu->addAction(tr("&About GoldenCheetah"), this, SLOT(aboutDialog()));
|
|
|
|
HelpWhatsThis *helpMenuHelp = new HelpWhatsThis(helpMenu);
|
|
helpMenu->setWhatsThis(helpMenuHelp->getWhatsThisText(HelpWhatsThis::MenuBar_Help));
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Lets go, choose latest ride and get GUI up and running
|
|
*--------------------------------------------------------------------*/
|
|
|
|
showTabbar(appsettings->value(NULL, GC_TABBAR, "0").toBool());
|
|
|
|
//XXX!!! We really do need a mechanism for showing if a ride needs saving...
|
|
//connect(this, SIGNAL(rideDirty()), this, SLOT(enableSaveButton()));
|
|
//connect(this, SIGNAL(rideClean()), this, SLOT(enableSaveButton()));
|
|
|
|
saveGCState(currentTab->context); // set to whatever we started with
|
|
selectAnalysis();
|
|
|
|
//grab focus
|
|
currentTab->setFocus();
|
|
|
|
installEventFilter(this);
|
|
|
|
// catch global config changes
|
|
connect(GlobalContext::context(), SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
|
|
configChanged(CONFIG_APPEARANCE);
|
|
|
|
init = true;
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Lets ask for telemetry and check for updates
|
|
*--------------------------------------------------------------------*/
|
|
|
|
#if !defined(OPENDATA_DISABLE)
|
|
OpenData::check(currentTab->context);
|
|
#else
|
|
fprintf(stderr, "OpenData disabled, secret not defined.\n"); fflush(stderr);
|
|
#endif
|
|
|
|
#ifdef GC_HAS_CLOUD_DB
|
|
telemetryClient = new CloudDBTelemetryClient();
|
|
if (appsettings->value(NULL, GC_ALLOW_TELEMETRY, "undefined").toString() == "undefined" ) {
|
|
// ask user if storing is allowed
|
|
|
|
// check for Telemetry Storage acceptance
|
|
CloudDBAcceptTelemetryDialog acceptDialog;
|
|
acceptDialog.setModal(true);
|
|
if (acceptDialog.exec() == QDialog::Accepted) {
|
|
telemetryClient->upsertTelemetry();
|
|
};
|
|
} else if (appsettings->value(NULL, GC_ALLOW_TELEMETRY, false).toBool()) {
|
|
telemetryClient->upsertTelemetry();
|
|
}
|
|
|
|
versionClient = new CloudDBVersionClient();
|
|
versionClient->informUserAboutLatestVersions();
|
|
|
|
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
/*----------------------------------------------------------------------
|
|
* GUI
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::setSplash(bool first)
|
|
{
|
|
// new frameless widget
|
|
splash = new QWidget(NULL);
|
|
|
|
// modal dialog with no parent so we set it up as a 'splash'
|
|
// because QSplashScreen doesn't seem to work (!!)
|
|
splash->setAttribute(Qt::WA_DeleteOnClose);
|
|
splash->setWindowFlags(splash->windowFlags() | Qt::FramelessWindowHint);
|
|
#ifdef Q_OS_LINUX
|
|
splash->setWindowFlags(splash->windowFlags() | Qt::X11BypassWindowManagerHint);
|
|
#endif
|
|
|
|
// put widgets on it
|
|
progress = new QLabel(splash);
|
|
progress->setAlignment(Qt::AlignCenter);
|
|
QHBoxLayout *l = new QHBoxLayout(splash);
|
|
l->setSpacing(0);
|
|
l->addWidget(progress);
|
|
|
|
// lets go
|
|
splash->setFixedSize(100 *dpiXFactor, 80 *dpiYFactor);
|
|
|
|
if (first) {
|
|
// middle of screen
|
|
splash->move(desktop->availableGeometry().center()-QPoint(50, 25));
|
|
} else {
|
|
// middle of mainwindow is appropriate
|
|
splash->move(geometry().center()-QPoint(50, 25));
|
|
}
|
|
splash->show();
|
|
|
|
// reset the splash counter
|
|
loading=1;
|
|
}
|
|
|
|
void
|
|
MainWindow::clearSplash()
|
|
{
|
|
progress = NULL;
|
|
splash->close();
|
|
}
|
|
|
|
void
|
|
MainWindow::toggleSidebar()
|
|
{
|
|
currentTab->toggleSidebar();
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::showSidebar(bool want)
|
|
{
|
|
currentTab->setSidebarEnabled(want);
|
|
showhideSidebar->setChecked(want);
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::toggleLowbar()
|
|
{
|
|
if (currentTab->hasBottom()) currentTab->setBottomRequested(!currentTab->isBottomRequested());
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::showLowbar(bool want)
|
|
{
|
|
if (currentTab->hasBottom()) currentTab->setBottomRequested(want);
|
|
showhideLowbar->setChecked(want);
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::enterWhatsThisMode()
|
|
{
|
|
QWhatsThis::enterWhatsThisMode();
|
|
}
|
|
|
|
void
|
|
MainWindow::showTabbar(bool want)
|
|
{
|
|
setUpdatesEnabled(false);
|
|
showhideTabbar->setChecked(want);
|
|
if (want) {
|
|
tabbar->show();
|
|
}
|
|
else {
|
|
tabbar->hide();
|
|
}
|
|
setUpdatesEnabled(true);
|
|
}
|
|
|
|
void
|
|
MainWindow::showToolbar(bool want)
|
|
{
|
|
setUpdatesEnabled(false);
|
|
showhideToolbar->setChecked(want);
|
|
if (want) {
|
|
head->show();
|
|
}
|
|
else {
|
|
head->hide();
|
|
}
|
|
setUpdatesEnabled(true);
|
|
}
|
|
|
|
void
|
|
MainWindow::setChartMenu()
|
|
{
|
|
unsigned int mask=0;
|
|
|
|
// called when chart menu about to be shown
|
|
// setup to only show charts that are relevant
|
|
// to this view
|
|
switch(currentTab->currentView()) {
|
|
case 0 : mask = VIEW_HOME; break;
|
|
default:
|
|
case 1 : mask = VIEW_ANALYSIS; break;
|
|
case 2 : mask = VIEW_DIARY; break;
|
|
case 3 : mask = VIEW_TRAIN; break;
|
|
case 4 : mask = VIEW_INTERVAL; break;
|
|
}
|
|
|
|
chartMenu->clear();
|
|
if (!mask) return;
|
|
|
|
for(int i=0; GcWindows[i].relevance; i++) {
|
|
if (GcWindows[i].relevance & mask)
|
|
chartMenu->addAction(GcWindows[i].name);
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::setSubChartMenu()
|
|
{
|
|
setChartMenu(subChartMenu);
|
|
}
|
|
|
|
void
|
|
MainWindow::setChartMenu(QMenu *menu)
|
|
{
|
|
unsigned int mask=0;
|
|
// called when chart menu about to be shown
|
|
// setup to only show charts that are relevant
|
|
// to this view
|
|
switch(currentTab->currentView()) {
|
|
case 0 : mask = VIEW_HOME; break;
|
|
default:
|
|
case 1 : mask = VIEW_ANALYSIS; break;
|
|
case 2 : mask = VIEW_DIARY; break;
|
|
case 3 : mask = VIEW_TRAIN; break;
|
|
case 4 : mask = VIEW_INTERVAL; break;
|
|
}
|
|
|
|
menu->clear();
|
|
if (!mask) return;
|
|
|
|
for(int i=0; GcWindows[i].relevance; i++) {
|
|
if (GcWindows[i].relevance & mask)
|
|
menu->addAction(GcWindows[i].name);
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::addChart(QAction*action)
|
|
{
|
|
// & removed to avoid issues with kde AutoCheckAccelerators
|
|
QString actionText = QString(action->text()).replace("&", "");
|
|
GcWinID id = GcWindowTypes::None;
|
|
for (int i=0; GcWindows[i].relevance; i++) {
|
|
if (GcWindows[i].name == actionText) {
|
|
id = GcWindows[i].id;
|
|
break;
|
|
}
|
|
}
|
|
if (id != GcWindowTypes::None)
|
|
currentTab->addChart(id); // called from MainWindow to inset chart
|
|
}
|
|
|
|
void
|
|
MainWindow::importChart()
|
|
{
|
|
QString fileName = QFileDialog::getOpenFileName(this, tr("Select Chart file to import"), "", tr("GoldenCheetah Chart Files (*.gchart)"));
|
|
|
|
if (fileName.isEmpty()) {
|
|
QMessageBox::critical(this, tr("Import Chart"), tr("No chart file selected!"));
|
|
} else {
|
|
importCharts(QStringList()<<fileName);
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::exportPerspective()
|
|
{
|
|
int view = currentTab->currentView();
|
|
TabView *current = NULL;
|
|
|
|
switch (view) {
|
|
case 0: current = currentTab->homeView; break;
|
|
case 1: current = currentTab->analysisView; break;
|
|
case 2: current = currentTab->diaryView; break;
|
|
case 3: current = currentTab->trainView; break;
|
|
}
|
|
|
|
// export the current perspective to a file
|
|
QString suffix;
|
|
QString fileName = QFileDialog::getSaveFileName(this, tr("Export Persepctive"),
|
|
QDir::homePath()+"/"+ current->perspective_->title() + ".gchartset",
|
|
("*.gchartset;;"), &suffix, QFileDialog::DontUseNativeDialog); // native dialog hangs when threads in use (!)
|
|
|
|
if (fileName.isEmpty()) {
|
|
QMessageBox::critical(this, tr("Export Perspective"), tr("No perspective file selected!"));
|
|
} else {
|
|
current->exportPerspective(current->perspective_, fileName);
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::importPerspective()
|
|
{
|
|
int view = currentTab->currentView();
|
|
TabView *current = NULL;
|
|
|
|
switch (view) {
|
|
case 0: current = currentTab->homeView; break;
|
|
case 1: current = currentTab->analysisView; break;
|
|
case 2: current = currentTab->diaryView; break;
|
|
case 3: current = currentTab->trainView; break;
|
|
}
|
|
|
|
// import a new perspective from a file
|
|
QString fileName = QFileDialog::getOpenFileName(this, tr("Select Perspective file to export"), "", tr("GoldenCheetah Perspective Files (*.gchartset)"));
|
|
if (fileName.isEmpty()) {
|
|
QMessageBox::critical(this, tr("Import Perspective"), tr("No perspective file selected!"));
|
|
} else {
|
|
|
|
// import and select it
|
|
pactive = true;
|
|
current->importPerspective(fileName);
|
|
current->setPerspectives(perspectiveSelector);
|
|
|
|
// and select remember pactive is true, so we do the heavy lifting here
|
|
perspectiveSelector->setCurrentIndex(current->perspectives_.count()-1);
|
|
current->perspectiveSelected(perspectiveSelector->currentIndex());
|
|
pactive = false;
|
|
}
|
|
|
|
}
|
|
|
|
#ifdef GC_HAS_CLOUD_DB
|
|
|
|
void
|
|
MainWindow::exportChartToCloudDB()
|
|
{
|
|
// upload the current chart selected to the chart db
|
|
// called from the sidebar menu
|
|
Perspective *page=currentTab->view(currentTab->currentView())->page();
|
|
if (page->currentStyle == 0 && page->currentChart())
|
|
page->currentChart()->exportChartToCloudDB();
|
|
}
|
|
|
|
void
|
|
MainWindow::addChartFromCloudDB()
|
|
{
|
|
if (!(appsettings->cvalue(currentTab->context->athlete->cyclist, GC_CLOUDDB_TC_ACCEPTANCE, false).toBool())) {
|
|
CloudDBAcceptConditionsDialog acceptDialog(currentTab->context->athlete->cyclist);
|
|
acceptDialog.setModal(true);
|
|
if (acceptDialog.exec() == QDialog::Rejected) {
|
|
return;
|
|
};
|
|
}
|
|
|
|
if (currentTab->context->cdbChartListDialog == NULL) {
|
|
currentTab->context->cdbChartListDialog = new CloudDBChartListDialog();
|
|
}
|
|
|
|
if (currentTab->context->cdbChartListDialog->prepareData(currentTab->context->athlete->cyclist, CloudDBCommon::UserImport, currentTab->currentView())) {
|
|
if (currentTab->context->cdbChartListDialog->exec() == QDialog::Accepted) {
|
|
|
|
// get selected chartDef
|
|
QList<QString> chartDefs = currentTab->context->cdbChartListDialog->getSelectedSettings();
|
|
|
|
// parse charts into property pairs
|
|
foreach (QString chartDef, chartDefs) {
|
|
QList<QMap<QString,QString> > properties = GcChartWindow::chartPropertiesFromString(chartDef);
|
|
for (int i = 0; i< properties.size(); i++) {
|
|
currentTab->context->mainWindow->athleteTab()->view(currentTab->currentView())->importChart(properties.at(i), false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void
|
|
MainWindow::setStyleFromSegment(int segment)
|
|
{
|
|
currentTab->setTiled(segment);
|
|
styleAction->setChecked(!segment);
|
|
}
|
|
|
|
void
|
|
MainWindow::toggleStyle()
|
|
{
|
|
currentTab->toggleTile();
|
|
styleAction->setChecked(currentTab->isTiled());
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::toggleFullScreen()
|
|
{
|
|
#ifdef Q_OS_MAC
|
|
QRect screenSize = desktop->availableGeometry();
|
|
if (screenSize.width() > frameGeometry().width() ||
|
|
screenSize.height() > frameGeometry().height())
|
|
showFullScreen();
|
|
else
|
|
showNormal();
|
|
#else
|
|
if (fullScreen) fullScreen->toggle();
|
|
else qDebug()<<"no fullscreen support compiled in.";
|
|
#endif
|
|
}
|
|
|
|
bool
|
|
MainWindow::eventFilter(QObject *o, QEvent *e)
|
|
{
|
|
if (o == this) {
|
|
if (e->type() == QEvent::WindowStateChange) resizeEvent(NULL); // see below
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MainWindow::resizeEvent(QResizeEvent*)
|
|
{
|
|
//appsettings->setValue(GC_SETTINGS_MAIN_GEOM, saveGeometry());
|
|
//appsettings->setValue(GC_SETTINGS_MAIN_STATE, saveState());
|
|
}
|
|
|
|
void
|
|
MainWindow::showOptions()
|
|
{
|
|
// Create a new config dialog only if it doesn't exist
|
|
ConfigDialog *cd = configdialog_ptr ? configdialog_ptr
|
|
: new ConfigDialog(currentTab->context->athlete->home->root(), currentTab->context);
|
|
|
|
// move to the centre of the screen
|
|
cd->move(geometry().center()-QPoint(cd->geometry().width()/2, cd->geometry().height()/2));
|
|
cd->show();
|
|
cd->raise();
|
|
}
|
|
|
|
void
|
|
MainWindow::moveEvent(QMoveEvent*)
|
|
{
|
|
appsettings->setValue(GC_SETTINGS_MAIN_GEOM, saveGeometry());
|
|
}
|
|
|
|
void
|
|
MainWindow::closeEvent(QCloseEvent* event)
|
|
{
|
|
QList<Tab*> closing = tabList;
|
|
bool needtosave = false;
|
|
bool importrunning = false;
|
|
|
|
// close all the tabs .. if any refuse we need to ignore
|
|
// the close event
|
|
foreach(Tab *tab, closing) {
|
|
|
|
// check for if RideImport is is process and let it finalize / or be stopped by the user
|
|
if (tab->context->athlete->autoImport) {
|
|
if (tab->context->athlete->autoImport->importInProcess() ) {
|
|
importrunning = true;
|
|
QMessageBox::information(this, tr("Activity Import"), tr("Closing of athlete window not possible while background activity import is in progress..."));
|
|
}
|
|
}
|
|
|
|
// only check for unsaved if autoimport is not running any more
|
|
if (!importrunning) {
|
|
// do we need to save?
|
|
if (tab->context->mainWindow->saveRideExitDialog(tab->context) == true)
|
|
removeTab(tab);
|
|
else
|
|
needtosave = true;
|
|
}
|
|
}
|
|
|
|
// were any left hanging around? or autoimport in action on any windows, then don't close any
|
|
if (needtosave || importrunning) event->ignore();
|
|
else {
|
|
|
|
// finish off the job and leave
|
|
// clear the clipboard if neccessary
|
|
QApplication::clipboard()->setText("");
|
|
|
|
// now remove from the list
|
|
if(mainwindows.removeOne(this) == false)
|
|
qDebug()<<"closeEvent: mainwindows list error";
|
|
|
|
// save global mainwindow settings
|
|
appsettings->setValue(GC_TABBAR, showhideTabbar->isChecked());
|
|
// wait for threads.. max of 10 seconds before just exiting anyway
|
|
for (int i=0; i<10 && QThreadPool::globalInstance()->activeThreadCount(); i++) {
|
|
QThread::sleep(1);
|
|
}
|
|
}
|
|
appsettings->setValue(GC_SETTINGS_MAIN_GEOM, saveGeometry());
|
|
appsettings->setValue(GC_SETTINGS_MAIN_STATE, saveState());
|
|
}
|
|
|
|
MainWindow::~MainWindow()
|
|
{
|
|
// aside from the tabs, we may need to clean
|
|
// up any dangling widgets created in MainWindow::MainWindow (?)
|
|
if (configdialog_ptr) configdialog_ptr->close();
|
|
}
|
|
|
|
// global search/data filter
|
|
void MainWindow::setFilter(QStringList f) { currentTab->context->setFilter(f); }
|
|
void MainWindow::clearFilter() { currentTab->context->clearFilter(); }
|
|
|
|
void
|
|
MainWindow::aboutDialog()
|
|
{
|
|
AboutDialog *ad = new AboutDialog(currentTab->context);
|
|
ad->exec();
|
|
}
|
|
|
|
void MainWindow::showSolveCP()
|
|
{
|
|
SolveCPDialog *td = new SolveCPDialog(this, currentTab->context);
|
|
td->show();
|
|
}
|
|
|
|
void MainWindow::showEstimateCP()
|
|
{
|
|
EstimateCPDialog *td = new EstimateCPDialog();
|
|
td->show();
|
|
}
|
|
|
|
void MainWindow::showRhoEstimator()
|
|
{
|
|
ToolsRhoEstimator *tre = new ToolsRhoEstimator(currentTab->context);
|
|
tre->show();
|
|
}
|
|
|
|
void MainWindow::showVDOTCalculator()
|
|
{
|
|
VDOTCalculator *VDOTcalculator = new VDOTCalculator();
|
|
VDOTcalculator->show();
|
|
}
|
|
|
|
void MainWindow::showWorkoutWizard()
|
|
{
|
|
WorkoutWizard *ww = new WorkoutWizard(currentTab->context);
|
|
ww->show();
|
|
}
|
|
|
|
void MainWindow::resetWindowLayout()
|
|
{
|
|
QMessageBox msgBox;
|
|
msgBox.setText(tr("You are about to reset all charts to the default setup"));
|
|
msgBox.setInformativeText(tr("Do you want to continue?"));
|
|
msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
|
|
msgBox.setDefaultButton(QMessageBox::Cancel);
|
|
msgBox.setIcon(QMessageBox::Warning);
|
|
msgBox.exec();
|
|
|
|
if(msgBox.clickedButton() == msgBox.button(QMessageBox::Ok))
|
|
currentTab->resetLayout();
|
|
}
|
|
|
|
void MainWindow::manualProcess(QString name)
|
|
{
|
|
// open a dialog box and let the users
|
|
// configure the options to use
|
|
// and also show the explanation
|
|
// of what this function does
|
|
// then call it!
|
|
RideItem *rideitem = (RideItem*)currentTab->context->currentRideItem();
|
|
if (rideitem) {
|
|
|
|
#ifdef GC_WANT_PYTHON
|
|
if (name.startsWith("_fix_py_")) {
|
|
name = name.remove(0, 8);
|
|
|
|
FixPyScript *script = fixPySettings->getScript(name);
|
|
if (script == nullptr) {
|
|
return;
|
|
}
|
|
|
|
QString errText;
|
|
FixPyRunner pyRunner(currentTab->context);
|
|
if (pyRunner.run(script->source, script->iniKey, errText) != 0) {
|
|
QMessageBox::critical(this, "GoldenCheetah", errText);
|
|
}
|
|
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
ManualDataProcessorDialog *p = new ManualDataProcessorDialog(currentTab->context, name, rideitem);
|
|
p->setWindowModality(Qt::ApplicationModal); // don't allow select other ride or it all goes wrong!
|
|
p->exec();
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
MainWindow::helpWindow()
|
|
{
|
|
HelpWindow* help = new HelpWindow(currentTab->context);
|
|
help->show();
|
|
}
|
|
|
|
|
|
void
|
|
MainWindow::logBug()
|
|
{
|
|
QDesktopServices::openUrl(QUrl("https://github.com/GoldenCheetah/GoldenCheetah/issues"));
|
|
}
|
|
|
|
void
|
|
MainWindow::helpView()
|
|
{
|
|
QDesktopServices::openUrl(QUrl("https://github.com/GoldenCheetah/GoldenCheetah/wiki"));
|
|
}
|
|
|
|
|
|
void
|
|
MainWindow::support()
|
|
{
|
|
QDesktopServices::openUrl(QUrl("https://groups.google.com/forum/#!forum/golden-cheetah-users"));
|
|
}
|
|
|
|
void
|
|
MainWindow::sidebarClicked(int id)
|
|
{
|
|
// sync quick link
|
|
if (id == 7) checkCloud();
|
|
|
|
// prefs
|
|
if (id == 8) showOptions();
|
|
|
|
}
|
|
|
|
void
|
|
MainWindow::sidebarSelected(int id)
|
|
{
|
|
switch (id) {
|
|
case 0: selectAthlete(); break;
|
|
case 1: // plan not written yet
|
|
break;
|
|
case 2: selectHome(); break;
|
|
case 3: selectAnalysis(); break;
|
|
case 4: // reflect not written yet
|
|
break;
|
|
case 5: selectTrain(); break;
|
|
case 6: // apps not written yet
|
|
break;
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::selectAthlete()
|
|
{
|
|
viewStack->setCurrentIndex(0);
|
|
perspectiveSelector->hide();
|
|
}
|
|
|
|
void
|
|
MainWindow::selectAnalysis()
|
|
{
|
|
currentTab->analysisView->setPerspectives(perspectiveSelector);
|
|
viewStack->setCurrentIndex(1);
|
|
sidebar->setItemSelected(3, true);
|
|
currentTab->selectView(1);
|
|
perspectiveSelector->show();
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::selectTrain()
|
|
{
|
|
currentTab->trainView->setPerspectives(perspectiveSelector);
|
|
viewStack->setCurrentIndex(1);
|
|
sidebar->setItemSelected(5, true);
|
|
currentTab->selectView(3);
|
|
perspectiveSelector->show();
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::selectDiary()
|
|
{
|
|
currentTab->diaryView->setPerspectives(perspectiveSelector);
|
|
viewStack->setCurrentIndex(1);
|
|
currentTab->selectView(2);
|
|
perspectiveSelector->show();
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::selectHome()
|
|
{
|
|
currentTab->homeView->setPerspectives(perspectiveSelector);
|
|
viewStack->setCurrentIndex(1);
|
|
sidebar->setItemSelected(2, true);
|
|
currentTab->selectView(0);
|
|
perspectiveSelector->show();
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::selectInterval()
|
|
{
|
|
currentTab->selectView(4);
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::setToolButtons()
|
|
{
|
|
int select = currentTab->isTiled() ? 1 : 0;
|
|
int lowselected = currentTab->isBottomRequested() ? 1 : 0;
|
|
|
|
styleAction->setChecked(select);
|
|
showhideLowbar->setChecked(lowselected);
|
|
|
|
if (styleSelector->isSegmentSelected(select) == false)
|
|
styleSelector->setSegmentSelected(select, true);
|
|
|
|
int index = currentTab->currentView();
|
|
|
|
//XXX WTAF! The index used is fucked up XXX
|
|
// hack around this and then come back
|
|
// and look at this as a separate fixup
|
|
#ifdef GC_HAVE_ICAL
|
|
switch (index) {
|
|
case 0: // home no change
|
|
case 3: // train no change
|
|
default:
|
|
break;
|
|
case 1:
|
|
index = 2; // analysis
|
|
break;
|
|
case 2:
|
|
index = 1; // diary
|
|
break;
|
|
}
|
|
#else
|
|
switch (index) {
|
|
case 0: // home no change
|
|
case 1:
|
|
default:
|
|
break;
|
|
case 3:
|
|
index = 2; // train
|
|
}
|
|
#endif
|
|
#ifdef Q_OS_MAC // bizarre issue with searchbox focus on tab voew change
|
|
searchBox->clearFocus();
|
|
#endif
|
|
}
|
|
|
|
void
|
|
MainWindow::perspectiveSelected(int index)
|
|
{
|
|
if (pactive) return;
|
|
|
|
// set the perspective for the current view
|
|
int view = currentTab->currentView();
|
|
TabView *current = NULL;
|
|
switch (view) {
|
|
case 0: current = currentTab->homeView; break;
|
|
case 1: current = currentTab->analysisView; break;
|
|
case 2: current = currentTab->diaryView; break;
|
|
case 3: current = currentTab->trainView; break;
|
|
}
|
|
|
|
// which perspective is currently being shown?
|
|
int prior = current->perspectives_.indexOf(current->perspective_);
|
|
|
|
if (index < current->perspectives_.count()) {
|
|
|
|
// a perspectives was selected
|
|
switch (view) {
|
|
case 0: current->perspectiveSelected(index); break;
|
|
case 1: current->perspectiveSelected(index); break;
|
|
case 2: current->perspectiveSelected(index); break;
|
|
case 3: current->perspectiveSelected(index); break;
|
|
}
|
|
|
|
} else {
|
|
|
|
// manage or add perspectives selected
|
|
pactive = true;
|
|
|
|
// set the combo back to where it was
|
|
perspectiveSelector->setCurrentIndex(prior);
|
|
|
|
// now open dialog etc
|
|
switch (index - current->perspectives_.count()) {
|
|
case 1 : // add perspectives
|
|
{
|
|
QString name;
|
|
AddPerspectiveDialog *dialog= new AddPerspectiveDialog(currentTab->context, name);
|
|
int ret= dialog->exec();
|
|
delete dialog;
|
|
if (ret == QDialog::Accepted && name != "") {
|
|
|
|
// add...
|
|
current->addPerspective(name);
|
|
current->setPerspectives(perspectiveSelector);
|
|
|
|
// and select remember pactive is true, so we do the heavy lifting here
|
|
perspectiveSelector->setCurrentIndex(current->perspectives_.count()-1);
|
|
current->perspectiveSelected(perspectiveSelector->currentIndex());
|
|
}
|
|
}
|
|
break;
|
|
case 2 : // manage perspectives
|
|
PerspectiveDialog *dialog = new PerspectiveDialog(this, current);
|
|
connect(dialog, SIGNAL(perspectivesChanged()), this, SLOT(perspectivesChanged())); // update the selector and view
|
|
dialog->exec();
|
|
break;
|
|
}
|
|
pactive = false;
|
|
}
|
|
}
|
|
|
|
// manage perspectives has done something (remove/add/reorder perspectives)
|
|
// pactive MUST be true, see above
|
|
void
|
|
MainWindow::perspectivesChanged()
|
|
{
|
|
int view = currentTab->currentView();
|
|
TabView *current = NULL;
|
|
|
|
switch (view) {
|
|
case 0: current = currentTab->homeView; break;
|
|
case 1: current = currentTab->analysisView; break;
|
|
case 2: current = currentTab->diaryView; break;
|
|
case 3: current = currentTab->trainView; break;
|
|
}
|
|
|
|
// which perspective is currently being selected (before we go setting the combobox)
|
|
Perspective *prior = current->perspective_;
|
|
|
|
// ok, so reset the combobox
|
|
current->setPerspectives(perspectiveSelector);
|
|
|
|
// is the old selected perspective still available?
|
|
int index = current->perspectives_.indexOf(prior);
|
|
|
|
// pretend a selection was made if the index needs to change
|
|
if (index >= 0 ) {
|
|
// still exists, but not currently selected for some reason
|
|
if (perspectiveSelector->currentIndex() != index)
|
|
perspectiveSelector->setCurrentIndex(index);
|
|
|
|
// no need to signal as its currently being shown
|
|
} else {
|
|
|
|
pactive = false; // dialog is active, but we need to force a change
|
|
|
|
// need to choose first as current got deleted
|
|
if (perspectiveSelector->currentIndex() != 0)
|
|
perspectiveSelector->setCurrentIndex(0);
|
|
else
|
|
emit perspectiveSelector->currentIndexChanged(0);
|
|
|
|
pactive = true;
|
|
}
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Drag and Drop
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::dragEnterEvent(QDragEnterEvent *event)
|
|
{
|
|
bool accept = true;
|
|
|
|
// we reject http, since we want a file!
|
|
foreach (QUrl url, event->mimeData()->urls())
|
|
if (url.toString().startsWith("http"))
|
|
accept = false;
|
|
|
|
if (accept) {
|
|
event->acceptProposedAction(); // whatever you wanna drop we will try and process!
|
|
raise();
|
|
} else event->ignore();
|
|
}
|
|
|
|
void
|
|
MainWindow::dropEvent(QDropEvent *event)
|
|
{
|
|
QList<QUrl> urls = event->mimeData()->urls();
|
|
if (urls.isEmpty()) return;
|
|
|
|
// is this a chart file ?
|
|
QStringList filenames;
|
|
QList<LTMSettings> imported;
|
|
QStringList list, workouts;
|
|
for(int i=0; i<urls.count(); i++) {
|
|
|
|
QString filename = QFileInfo(urls.value(i).toLocalFile()).absoluteFilePath();
|
|
fprintf(stderr, "%s\n", filename.toStdString().c_str()); fflush(stderr);
|
|
|
|
if (filename.endsWith(".gchart", Qt::CaseInsensitive)) {
|
|
// add to the list of charts to import
|
|
list << filename;
|
|
|
|
} else if (filename.endsWith(".xml", Qt::CaseInsensitive)) {
|
|
|
|
QFile chartsFile(filename);
|
|
|
|
// setup XML processor
|
|
QXmlInputSource source( &chartsFile );
|
|
QXmlSimpleReader xmlReader;
|
|
LTMChartParser handler;
|
|
xmlReader.setContentHandler(&handler);
|
|
xmlReader.setErrorHandler(&handler);
|
|
|
|
// parse and get return values
|
|
xmlReader.parse(source);
|
|
imported += handler.getSettings();
|
|
|
|
// Look for Workout files only in Train view
|
|
} else if (currentTab->currentView() == 3 && ErgFile::isWorkout(filename)) {
|
|
workouts << filename;
|
|
} else {
|
|
filenames.append(filename);
|
|
}
|
|
}
|
|
|
|
// import any we may have extracted
|
|
if (imported.count()) {
|
|
|
|
// now append to the QList and QTreeWidget
|
|
currentTab->context->athlete->presets += imported;
|
|
|
|
// notify we changed and tree updates
|
|
currentTab->context->notifyPresetsChanged();
|
|
|
|
// tell the user
|
|
QMessageBox::information(this, tr("Chart Import"), QString(tr("Imported %1 metric charts")).arg(imported.count()));
|
|
|
|
// switch to trend view if we aren't on it
|
|
selectHome();
|
|
|
|
// now select what was added
|
|
currentTab->context->notifyPresetSelected(currentTab->context->athlete->presets.count()-1);
|
|
}
|
|
|
|
// are there any .gcharts to import?
|
|
if (list.count()) importCharts(list);
|
|
|
|
// import workouts
|
|
if (workouts.count()) Library::importFiles(currentTab->context, workouts, true);
|
|
|
|
// if there is anything left, process based upon view...
|
|
if (filenames.count()) {
|
|
|
|
// We have something to process then
|
|
RideImportWizard *dialog = new RideImportWizard (filenames, currentTab->context);
|
|
dialog->process(); // do it!
|
|
}
|
|
return;
|
|
}
|
|
|
|
void
|
|
MainWindow::importCharts(QStringList list)
|
|
{
|
|
QList<QMap<QString,QString> > charts;
|
|
|
|
// parse charts into property pairs
|
|
foreach(QString filename, list) {
|
|
charts << GcChartWindow::chartPropertiesFromFile(filename);
|
|
}
|
|
|
|
// And import them with a dialog to select location
|
|
ImportChartDialog importer(currentTab->context, charts, this);
|
|
importer.exec();
|
|
}
|
|
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Ride Library Functions
|
|
*--------------------------------------------------------------------*/
|
|
|
|
|
|
void
|
|
MainWindow::downloadRide()
|
|
{
|
|
(new DownloadRideDialog(currentTab->context))->show();
|
|
}
|
|
|
|
|
|
void
|
|
MainWindow::manualRide()
|
|
{
|
|
(new ManualRideDialog(currentTab->context))->show();
|
|
}
|
|
|
|
void
|
|
MainWindow::exportBatch()
|
|
{
|
|
BatchExportDialog *d = new BatchExportDialog(currentTab->context);
|
|
d->exec();
|
|
}
|
|
|
|
void
|
|
MainWindow::generateHeatMap()
|
|
{
|
|
GenerateHeatMapDialog *d = new GenerateHeatMapDialog(currentTab->context);
|
|
d->exec();
|
|
}
|
|
|
|
void
|
|
MainWindow::exportRide()
|
|
{
|
|
if (currentTab->context->ride == NULL) {
|
|
QMessageBox::critical(this, tr("Select Activity"), tr("No activity selected!"));
|
|
return;
|
|
}
|
|
|
|
// what format?
|
|
const RideFileFactory &rff = RideFileFactory::instance();
|
|
QStringList allFormats;
|
|
allFormats << "Export all data (*.csv)";
|
|
allFormats << "Export W' balance (*.csv)";
|
|
foreach(QString suffix, rff.writeSuffixes())
|
|
allFormats << QString("%1 (*.%2)").arg(rff.description(suffix)).arg(suffix);
|
|
|
|
QString suffix; // what was selected?
|
|
QString fileName = QFileDialog::getSaveFileName(this, tr("Export Activity"), QDir::homePath(), allFormats.join(";;"), &suffix);
|
|
|
|
if (fileName.length() == 0) return;
|
|
|
|
// which file type was selected
|
|
// extract from the suffix returned
|
|
QRegExp getSuffix("^[^(]*\\(\\*\\.([^)]*)\\)$");
|
|
getSuffix.exactMatch(suffix);
|
|
|
|
QFile file(fileName);
|
|
RideFile *currentRide = currentTab->context->ride ? currentTab->context->ride->ride() : NULL;
|
|
bool result=false;
|
|
|
|
// Extract suffix from chosen file name and convert to lower case
|
|
QString fileNameSuffix;
|
|
int lastDot = fileName.lastIndexOf(".");
|
|
if (lastDot >=0) fileNameSuffix = fileName.mid(fileName.lastIndexOf(".")+1);
|
|
fileNameSuffix = fileNameSuffix.toLower();
|
|
|
|
// See if this suffix can be used to determine file type (not ok for csv since there are options)
|
|
bool useFileTypeSuffix = false;
|
|
if (!fileNameSuffix.isEmpty() && fileNameSuffix != "csv")
|
|
{
|
|
useFileTypeSuffix = rff.writeSuffixes().contains(fileNameSuffix);
|
|
}
|
|
|
|
if (useFileTypeSuffix)
|
|
{
|
|
result = RideFileFactory::instance().writeRideFile(currentTab->context, currentRide, file, fileNameSuffix);
|
|
}
|
|
else
|
|
{
|
|
// Use the value of drop down list to determine file type
|
|
int idx = allFormats.indexOf(getSuffix.cap(0));
|
|
|
|
if (idx>1) {
|
|
|
|
result = RideFileFactory::instance().writeRideFile(currentTab->context, currentRide, file, getSuffix.cap(1));
|
|
|
|
} else if (idx==0){
|
|
|
|
CsvFileReader writer;
|
|
result = writer.writeRideFile(currentTab->context, currentRide, file, CsvFileReader::gc);
|
|
|
|
} else if (idx==1){
|
|
|
|
CsvFileReader writer;
|
|
result = writer.writeRideFile(currentTab->context, currentRide, file, CsvFileReader::wprime);
|
|
|
|
}
|
|
}
|
|
|
|
if (result == false) {
|
|
QMessageBox oops(QMessageBox::Critical, tr("Export Failed"),
|
|
tr("Failed to export activity, please check permissions"));
|
|
oops.exec();
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::importFile()
|
|
{
|
|
QVariant lastDirVar = appsettings->value(this, GC_SETTINGS_LAST_IMPORT_PATH);
|
|
QString lastDir = (lastDirVar != QVariant())
|
|
? lastDirVar.toString() : QDir::homePath();
|
|
|
|
const RideFileFactory &rff = RideFileFactory::instance();
|
|
QStringList suffixList = rff.suffixes();
|
|
suffixList.replaceInStrings(QRegExp("^"), "*.");
|
|
QStringList fileNames;
|
|
QStringList allFormats;
|
|
allFormats << QString("All Supported Formats (%1)").arg(suffixList.join(" "));
|
|
foreach(QString suffix, rff.suffixes())
|
|
allFormats << QString("%1 (*.%2)").arg(rff.description(suffix)).arg(suffix);
|
|
allFormats << "All files (*.*)";
|
|
fileNames = QFileDialog::getOpenFileNames( this, tr("Import from File"), lastDir, allFormats.join(";;"));
|
|
if (!fileNames.isEmpty()) {
|
|
lastDir = QFileInfo(fileNames.front()).absolutePath();
|
|
appsettings->setValue(GC_SETTINGS_LAST_IMPORT_PATH, lastDir);
|
|
QStringList fileNamesCopy = fileNames; // QT doc says iterate over a copy
|
|
RideImportWizard *import = new RideImportWizard(fileNamesCopy, currentTab->context);
|
|
import->process();
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::saveRide()
|
|
{
|
|
// no ride
|
|
if (currentTab->context->ride == NULL) {
|
|
QMessageBox oops(QMessageBox::Critical, tr("No Activity To Save"),
|
|
tr("There is no currently selected activity to save."));
|
|
oops.exec();
|
|
return;
|
|
}
|
|
|
|
// flush in-flight changes
|
|
currentTab->context->notifyMetadataFlush();
|
|
currentTab->context->ride->notifyRideMetadataChanged();
|
|
|
|
// nothing to do if not dirty
|
|
//XXX FORCE A SAVE if (currentTab->context->ride->isDirty() == false) return;
|
|
|
|
// save
|
|
if (currentTab->context->ride) {
|
|
saveRideSingleDialog(currentTab->context, currentTab->context->ride); // will signal save to everyone
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::saveAllUnsavedRides()
|
|
{
|
|
// flush in-flight changes
|
|
currentTab->context->notifyMetadataFlush();
|
|
currentTab->context->ride->notifyRideMetadataChanged();
|
|
|
|
// save
|
|
if (currentTab->context->ride) {
|
|
saveAllFilesSilent(currentTab->context); // will signal save to everyone
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::revertRide()
|
|
{
|
|
currentTab->context->ride->close();
|
|
currentTab->context->ride->ride(); // force re-load
|
|
|
|
// in case reverted ride has different starttime
|
|
currentTab->context->ride->setStartTime(currentTab->context->ride->ride()->startTime());
|
|
currentTab->context->ride->ride()->emitReverted();
|
|
|
|
// and notify everyone we changed which also has the side
|
|
// effect of updating the cached values too
|
|
currentTab->context->notifyRideSelected(currentTab->context->ride);
|
|
}
|
|
|
|
void
|
|
MainWindow::splitRide()
|
|
{
|
|
if (currentTab->context->ride && currentTab->context->ride->ride() && currentTab->context->ride->ride()->dataPoints().count()) (new SplitActivityWizard(currentTab->context))->exec();
|
|
else {
|
|
if (!currentTab->context->ride || !currentTab->context->ride->ride())
|
|
QMessageBox::critical(this, tr("Split Activity"), tr("No activity selected"));
|
|
else
|
|
QMessageBox::critical(this, tr("Split Activity"), tr("Current activity contains no data to split"));
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::mergeRide()
|
|
{
|
|
if (currentTab->context->ride && currentTab->context->ride->ride() && currentTab->context->ride->ride()->dataPoints().count()) (new MergeActivityWizard(currentTab->context))->exec();
|
|
else {
|
|
if (!currentTab->context->ride || !currentTab->context->ride->ride())
|
|
QMessageBox::critical(this, tr("Split Activity"), tr("No activity selected"));
|
|
else
|
|
QMessageBox::critical(this, tr("Split Activity"), tr("Current activity contains no data to merge"));
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::deleteRide()
|
|
{
|
|
RideItem *_item = currentTab->context->ride;
|
|
|
|
if (_item==NULL) {
|
|
QMessageBox::critical(this, tr("Delete Activity"), tr("No activity selected!"));
|
|
return;
|
|
}
|
|
|
|
RideItem *item = static_cast<RideItem*>(_item);
|
|
QMessageBox msgBox;
|
|
msgBox.setText(tr("Are you sure you want to delete the activity:"));
|
|
msgBox.setInformativeText(item->fileName);
|
|
QPushButton *deleteButton = msgBox.addButton(tr("Delete"),QMessageBox::YesRole);
|
|
msgBox.setStandardButtons(QMessageBox::Cancel);
|
|
msgBox.setDefaultButton(QMessageBox::Cancel);
|
|
msgBox.setIcon(QMessageBox::Critical);
|
|
msgBox.exec();
|
|
if(msgBox.clickedButton() == deleteButton)
|
|
currentTab->context->athlete->removeCurrentRide();
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Realtime Devices and Workouts
|
|
*--------------------------------------------------------------------*/
|
|
void
|
|
MainWindow::addDevice()
|
|
{
|
|
|
|
// lets get a new one
|
|
AddDeviceWizard *p = new AddDeviceWizard(currentTab->context);
|
|
p->show();
|
|
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Cyclists
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::newCyclistTab()
|
|
{
|
|
QDir newHome = currentTab->context->athlete->home->root();
|
|
newHome.cdUp();
|
|
QString name = ChooseCyclistDialog::newCyclistDialog(newHome, this);
|
|
if (!name.isEmpty()) {
|
|
emit newAthlete(name);
|
|
openTab(name);
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::closeWindow()
|
|
{
|
|
// just call close, we might do more later
|
|
appsettings->syncQSettings();
|
|
close();
|
|
}
|
|
|
|
void
|
|
MainWindow::openTab(QString name)
|
|
{
|
|
QDir home(gcroot);
|
|
appsettings->initializeQSettingsGlobal(gcroot);
|
|
home.cd(name);
|
|
|
|
if (!home.exists()) return;
|
|
appsettings->initializeQSettingsAthlete(gcroot, name);
|
|
|
|
GcUpgrade v3;
|
|
if (!v3.upgradeConfirmedByUser(home)) return;
|
|
|
|
Context *con= new Context(this);
|
|
con->athlete = NULL;
|
|
emit openingAthlete(name, con);
|
|
|
|
connect(con, SIGNAL(loadCompleted(QString,Context*)), this, SLOT(loadCompleted(QString, Context*)));
|
|
|
|
// will emit loadCompleted when done
|
|
con->athlete = new Athlete(con, home);
|
|
}
|
|
|
|
void
|
|
MainWindow::loadCompleted(QString name, Context *context)
|
|
{
|
|
// athlete loaded
|
|
currentTab = new Tab(context);
|
|
|
|
// clear splash - progress whilst loading tab
|
|
//clearSplash();
|
|
|
|
// first tab
|
|
tabs.insert(currentTab->context->athlete->home->root().dirName(), currentTab);
|
|
|
|
// stack, list and bar all share a common index
|
|
tabList.append(currentTab);
|
|
tabbar->addTab(currentTab->context->athlete->home->root().dirName());
|
|
tabStack->addWidget(currentTab);
|
|
|
|
// switch to newly created athlete
|
|
tabbar->setCurrentIndex(tabList.count()-1);
|
|
|
|
// show the tabbar if we're gonna open tabs -- but wait till the last second
|
|
// to show it to avoid crappy paint artefacts
|
|
showTabbar(true);
|
|
|
|
// tell everyone
|
|
currentTab->context->notifyLoadDone(name, context);
|
|
|
|
// now do the automatic ride file import
|
|
context->athlete->importFilesWhenOpeningAthlete();
|
|
}
|
|
|
|
void
|
|
MainWindow::closeTabClicked(int index)
|
|
{
|
|
|
|
Tab *tab = tabList[index];
|
|
|
|
// check for autoimport and let it finalize
|
|
if (tab->context->athlete->autoImport) {
|
|
if (tab->context->athlete->autoImport->importInProcess() ) {
|
|
QMessageBox::information(this, tr("Activity Import"), tr("Closing of athlete window not possible while background activity import is in progress..."));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (saveRideExitDialog(tab->context) == false) return;
|
|
|
|
// lets wipe it
|
|
removeTab(tab);
|
|
}
|
|
|
|
bool
|
|
MainWindow::closeTab(QString name)
|
|
{
|
|
for(int i=0; i<tabbar->count(); i++) {
|
|
if (name == tabbar->tabText(i)) {
|
|
closeTabClicked(i);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MainWindow::closeTab()
|
|
{
|
|
// check for autoimport and let it finalize
|
|
if (currentTab->context->athlete->autoImport) {
|
|
if (currentTab->context->athlete->autoImport->importInProcess() ) {
|
|
QMessageBox::information(this, tr("Activity Import"), tr("Closing of athlete window not possible while background activity import is in progress..."));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// wipe it down ...
|
|
if (saveRideExitDialog(currentTab->context) == false) return false;
|
|
|
|
// if its the last tab we close the window
|
|
if (tabList.count() == 1)
|
|
closeWindow();
|
|
else {
|
|
removeTab(currentTab);
|
|
}
|
|
appsettings->syncQSettings();
|
|
// we did it
|
|
return true;
|
|
}
|
|
|
|
// no questions asked just wipe away the current tab
|
|
void
|
|
MainWindow::removeTab(Tab *tab)
|
|
{
|
|
setUpdatesEnabled(false);
|
|
|
|
if (tabList.count() == 2) showTabbar(false); // don't need it for one!
|
|
|
|
// cancel ridecache refresh if its in progress
|
|
tab->context->athlete->rideCache->cancel();
|
|
|
|
// save the named searches
|
|
tab->context->athlete->namedSearches->write();
|
|
|
|
// clear the clipboard if neccessary
|
|
QApplication::clipboard()->setText("");
|
|
|
|
// Remember where we were
|
|
QString name = tab->context->athlete->cyclist;
|
|
|
|
// switch to neighbour (currentTab will change)
|
|
int index = tabList.indexOf(tab);
|
|
|
|
// if we're not the last then switch
|
|
// before removing so the GUI is clean
|
|
if (tabList.count() > 1) {
|
|
if (index) switchTab(index-1);
|
|
else switchTab(index+1);
|
|
}
|
|
|
|
// close gracefully
|
|
tab->close();
|
|
tab->context->athlete->close();
|
|
|
|
// remove from state
|
|
tabs.remove(name);
|
|
tabList.removeAt(index);
|
|
tabbar->removeTab(index);
|
|
tabStack->removeWidget(tab);
|
|
|
|
// delete the objects
|
|
Context *context = tab->context;
|
|
Athlete *athlete = tab->context->athlete;
|
|
|
|
delete tab;
|
|
delete athlete;
|
|
delete context;
|
|
|
|
setUpdatesEnabled(true);
|
|
|
|
return;
|
|
}
|
|
|
|
void
|
|
MainWindow::setOpenTabMenu()
|
|
{
|
|
// wipe existing
|
|
openTabMenu->clear();
|
|
|
|
// get a list of all cyclists
|
|
QStringListIterator i(QDir(gcroot).entryList(QDir::Dirs | QDir::NoDotAndDotDot));
|
|
while (i.hasNext()) {
|
|
|
|
QString name = i.next();
|
|
SKIP_QTWE_CACHE // skip Folder Names created by QTWebEngine on Windows
|
|
// new action
|
|
QAction *action = new QAction(QString("%1").arg(name), this);
|
|
|
|
// get the config directory
|
|
AthleteDirectoryStructure subDirs(name);
|
|
// icon / mugshot ?
|
|
QString icon = QString("%1/%2/%3/avatar.png").arg(gcroot).arg(name).arg(subDirs.config().dirName());
|
|
if (QFile(icon).exists()) action->setIcon(QIcon(icon));
|
|
|
|
// only allow selection of cyclists which are not already open
|
|
foreach (MainWindow *x, mainwindows) {
|
|
QMapIterator<QString, Tab*> t(x->tabs);
|
|
while (t.hasNext()) {
|
|
t.next();
|
|
if (t.key() == name)
|
|
action->setEnabled(false);
|
|
}
|
|
}
|
|
|
|
// add to menu
|
|
openTabMenu->addAction(action);
|
|
connect(action, SIGNAL(triggered()), tabMapper, SLOT(map()));
|
|
tabMapper->setMapping(action, name);
|
|
}
|
|
|
|
// add create new option
|
|
openTabMenu->addSeparator();
|
|
openTabMenu->addAction(tr("&New Athlete..."), this, SLOT(newCyclistTab()), tr("Ctrl+N"));
|
|
}
|
|
|
|
void
|
|
MainWindow::setBackupAthleteMenu()
|
|
{
|
|
// wipe existing
|
|
backupAthleteMenu->clear();
|
|
|
|
// get a list of all cyclists
|
|
QStringListIterator i(QDir(gcroot).entryList(QDir::Dirs | QDir::NoDotAndDotDot));
|
|
while (i.hasNext()) {
|
|
|
|
QString name = i.next();
|
|
SKIP_QTWE_CACHE // skip Folder Names created by QTWebEngine on Windows
|
|
// new action
|
|
QAction *action = new QAction(QString("%1").arg(name), this);
|
|
|
|
// get the config directory
|
|
AthleteDirectoryStructure subDirs(name);
|
|
// icon / mugshot ?
|
|
QString icon = QString("%1/%2/%3/avatar.png").arg(gcroot).arg(name).arg(subDirs.config().dirName());
|
|
if (QFile(icon).exists()) action->setIcon(QIcon(icon));
|
|
|
|
// add to menu
|
|
backupAthleteMenu->addAction(action);
|
|
connect(action, SIGNAL(triggered()), backupMapper, SLOT(map()));
|
|
backupMapper->setMapping(action, name);
|
|
}
|
|
|
|
}
|
|
|
|
void
|
|
MainWindow::backupAthlete(QString name)
|
|
{
|
|
AthleteBackup *backup = new AthleteBackup(QDir(gcroot+"/"+name));
|
|
backup->backupImmediate();
|
|
delete backup;
|
|
}
|
|
|
|
void
|
|
MainWindow::setDeleteAthleteMenu()
|
|
{
|
|
// wipe existing
|
|
deleteAthleteMenu->clear();
|
|
|
|
// get a list of all cyclists
|
|
QStringListIterator i(QDir(gcroot).entryList(QDir::Dirs | QDir::NoDotAndDotDot));
|
|
while (i.hasNext()) {
|
|
|
|
QString name = i.next();
|
|
SKIP_QTWE_CACHE // skip Folder Names created by QTWebEngine on Windows
|
|
// new action
|
|
QAction *action = new QAction(QString("%1").arg(name), this);
|
|
|
|
// get the config directory
|
|
AthleteDirectoryStructure subDirs(name);
|
|
// icon / mugshot ?
|
|
QString icon = QString("%1/%2/%3/avatar.png").arg(gcroot).arg(name).arg(subDirs.config().dirName());
|
|
if (QFile(icon).exists()) action->setIcon(QIcon(icon));
|
|
|
|
// only allow selection of cyclists which are not already open
|
|
foreach (MainWindow *x, mainwindows) {
|
|
QMapIterator<QString, Tab*> t(x->tabs);
|
|
while (t.hasNext()) {
|
|
t.next();
|
|
if (t.key() == name)
|
|
action->setEnabled(false);
|
|
}
|
|
}
|
|
|
|
// add to menu
|
|
deleteAthleteMenu->addAction(action);
|
|
connect(action, SIGNAL(triggered()), deleteMapper, SLOT(map()));
|
|
deleteMapper->setMapping(action, name);
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::deleteAthlete(QString name)
|
|
{
|
|
QDir home(gcroot);
|
|
if(ChooseCyclistDialog::deleteAthlete(home, name, this)) {
|
|
// notify deletion
|
|
emit deletedAthlete(name);
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::saveGCState(Context *context)
|
|
{
|
|
// save all the current state to the supplied context
|
|
context->showSidebar = showhideSidebar->isChecked();
|
|
//context->showTabbar = showhideTabbar->isChecked();
|
|
context->showLowbar = showhideLowbar->isChecked();
|
|
context->showToolbar = showhideToolbar->isChecked();
|
|
context->searchText = searchBox->text();
|
|
context->style = styleAction->isChecked();
|
|
}
|
|
|
|
void
|
|
MainWindow::restoreGCState(Context *context)
|
|
{
|
|
// restore window state from the supplied context
|
|
showSidebar(context->showSidebar);
|
|
showToolbar(context->showToolbar);
|
|
//showTabbar(context->showTabbar);
|
|
showLowbar(context->showLowbar);
|
|
searchBox->setContext(context);
|
|
searchBox->setText(context->searchText);
|
|
}
|
|
|
|
void
|
|
MainWindow::switchTab(int index)
|
|
{
|
|
if (index < 0) return;
|
|
|
|
setUpdatesEnabled(false);
|
|
|
|
#if 0 // use athlete view, these buttons don't exist
|
|
#ifdef Q_OS_MAC // close buttons on the left on Mac
|
|
// Only have close button on current tab (prettier)
|
|
for(int i=0; i<tabbar->count(); i++) tabbar->tabButton(i, QTabBar::LeftSide)->hide();
|
|
tabbar->tabButton(index, QTabBar::LeftSide)->show();
|
|
#else
|
|
// Only have close button on current tab (prettier)
|
|
for(int i=0; i<tabbar->count(); i++) tabbar->tabButton(i, QTabBar::RightSide)->hide();
|
|
tabbar->tabButton(index, QTabBar::RightSide)->show();
|
|
#endif
|
|
#endif
|
|
|
|
// save how we are
|
|
saveGCState(currentTab->context);
|
|
|
|
currentTab = tabList[index];
|
|
tabStack->setCurrentIndex(index);
|
|
|
|
// restore back
|
|
restoreGCState(currentTab->context);
|
|
|
|
setWindowTitle(currentTab->context->athlete->home->root().dirName());
|
|
|
|
setUpdatesEnabled(true);
|
|
}
|
|
|
|
|
|
/*----------------------------------------------------------------------
|
|
* MetricDB
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::exportMetrics()
|
|
{
|
|
// if the refresh process is running, try again when its completed
|
|
if (currentTab->context->athlete->rideCache->isRunning()) {
|
|
QMessageBox::warning(this, tr("Refresh in Progress"),
|
|
"A metric refresh is currently running, please try again once that has completed.");
|
|
return;
|
|
}
|
|
|
|
// all good lets choose a file
|
|
QString fileName = QFileDialog::getSaveFileName( this, tr("Export Metrics"), QDir::homePath(), tr("Comma Separated Variables (*.csv)"));
|
|
if (fileName.length() == 0) return;
|
|
|
|
// export
|
|
currentTab->context->athlete->rideCache->writeAsCSV(fileName);
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Import Workout from Disk
|
|
*--------------------------------------------------------------------*/
|
|
void
|
|
MainWindow::importWorkout()
|
|
{
|
|
// go look at last place we imported workouts from...
|
|
QVariant lastDirVar = appsettings->value(this, GC_SETTINGS_LAST_WORKOUT_PATH);
|
|
QString lastDir = (lastDirVar != QVariant())
|
|
? lastDirVar.toString() : QDir::homePath();
|
|
|
|
// anything for now, we could add filters later
|
|
QStringList allFormats;
|
|
allFormats << "All files (*.*)";
|
|
QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Import from File"), lastDir, allFormats.join(";;"));
|
|
|
|
// lets process them
|
|
if (!fileNames.isEmpty()) {
|
|
|
|
// save away last place we looked
|
|
lastDir = QFileInfo(fileNames.front()).absolutePath();
|
|
appsettings->setValue(GC_SETTINGS_LAST_WORKOUT_PATH, lastDir);
|
|
|
|
QStringList fileNamesCopy = fileNames; // QT doc says iterate over a copy
|
|
|
|
// import them via the workoutimporter
|
|
Library::importFiles(currentTab->context, fileNamesCopy);
|
|
}
|
|
}
|
|
/*----------------------------------------------------------------------
|
|
* ErgDB
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::downloadErgDB()
|
|
{
|
|
QString workoutDir = appsettings->value(this, GC_WORKOUTDIR).toString();
|
|
|
|
QFileInfo fi(workoutDir);
|
|
|
|
if (fi.exists() && fi.isDir()) {
|
|
ErgDBDownloadDialog *d = new ErgDBDownloadDialog(currentTab->context);
|
|
d->exec();
|
|
} else{
|
|
QMessageBox::critical(this, tr("Workout Directory Invalid"),
|
|
tr("The workout directory is not configured, or the directory selected no longer exists.\n\n"
|
|
"Please check your preference settings."));
|
|
}
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* TodaysPlan Workouts
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::downloadTodaysPlanWorkouts()
|
|
{
|
|
QString workoutDir = appsettings->value(this, GC_WORKOUTDIR).toString();
|
|
|
|
QFileInfo fi(workoutDir);
|
|
|
|
if (fi.exists() && fi.isDir()) {
|
|
TodaysPlanWorkoutDownload *d = new TodaysPlanWorkoutDownload(currentTab->context);
|
|
d->exec();
|
|
} else{
|
|
QMessageBox::critical(this, tr("Workout Directory Invalid"),
|
|
tr("The workout directory is not configured, or the directory selected no longer exists.\n\n"
|
|
"Please check your preference settings."));
|
|
}
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Workout/Media Library
|
|
*--------------------------------------------------------------------*/
|
|
void
|
|
MainWindow::manageLibrary()
|
|
{
|
|
LibrarySearchDialog *search = new LibrarySearchDialog(currentTab->context);
|
|
search->exec();
|
|
}
|
|
|
|
/*----------------------------------------------------------------------------
|
|
* Working with Cloud Services
|
|
* --------------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::addAccount()
|
|
{
|
|
// lets get a new cloud service account
|
|
AddCloudWizard *p = new AddCloudWizard(currentTab->context);
|
|
p->show();
|
|
}
|
|
|
|
void
|
|
MainWindow::checkCloud()
|
|
{
|
|
// kick off a check
|
|
currentTab->context->athlete->cloudAutoDownload->checkDownload();
|
|
|
|
// and auto import too whilst we're at it
|
|
currentTab->context->athlete->importFilesWhenOpeningAthlete();
|
|
}
|
|
|
|
void
|
|
MainWindow::importCloud()
|
|
{
|
|
// lets get a new cloud service account
|
|
AddCloudWizard *p = new AddCloudWizard(currentTab->context, "", true);
|
|
p->show();
|
|
}
|
|
|
|
void
|
|
MainWindow::uploadCloud(QAction *action)
|
|
{
|
|
// upload current ride, if we have one
|
|
if (currentTab->context->ride) {
|
|
// & removed to avoid issues with kde AutoCheckAccelerators
|
|
QString actionText = QString(action->text()).replace("&", "");
|
|
|
|
if (actionText == "University of Kent") {
|
|
CloudService *db = CloudServiceFactory::instance().newService(action->data().toString(), currentTab->context);
|
|
KentUniversityUploadDialog uploader(this, db, currentTab->context->ride);
|
|
int ret = uploader.exec();
|
|
} else {
|
|
CloudService *db = CloudServiceFactory::instance().newService(action->data().toString(), currentTab->context);
|
|
CloudService::upload(this, currentTab->context, db, currentTab->context->ride);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::syncCloud(QAction *action)
|
|
{
|
|
// sync with cloud
|
|
CloudService *db = CloudServiceFactory::instance().newService(action->data().toString(), currentTab->context);
|
|
CloudServiceSyncDialog sync(currentTab->context, db);
|
|
sync.exec();
|
|
}
|
|
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Utility
|
|
*--------------------------------------------------------------------*/
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Notifiers - application level events
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::configChanged(qint32)
|
|
{
|
|
|
|
#if defined (WIN32) || defined (Q_OS_LINUX)
|
|
// Windows and Linux menu bar should match chrome
|
|
QColor textCol(Qt::black);
|
|
if (GCColor::luminance(GColor(CCHROME)) < 127) textCol = QColor(Qt::white);
|
|
QString menuColorString = (GCColor::isFlat() ? GColor(CCHROME).name() : "rgba(225,225,225)");
|
|
menuBar()->setStyleSheet(QString("QMenuBar { color: %1; background: %2; }"
|
|
"QMenuBar::item { color: %1; background: %2; }")
|
|
.arg(textCol.name()).arg(menuColorString));
|
|
// search filter box match chrome color
|
|
searchBox->setStyleSheet(QString("QLineEdit { background: %1; color: %2; }").arg(GColor(CCHROME).name()).arg(GCColor::invertColor(GColor(CCHROME)).name()));
|
|
|
|
// perspective selector mimics sidebar colors
|
|
QColor selected;
|
|
if (GCColor::invertColor(GColor(CCHROME)).name() == Qt::white) selected = QColor(Qt::lightGray);
|
|
else selected = QColor(Qt::darkGray);
|
|
perspectiveSelector->setStyleSheet(QString("QComboBox { background: %1; color: %2; }"
|
|
"QComboBox::item { background: %1; color: %2; }"
|
|
"QComboBox::item::selected { background: %3; color: %1; }").arg(GColor(CCHROME).name()).arg(GCColor::invertColor(GColor(CCHROME)).name()).arg(selected.name()));
|
|
|
|
#endif
|
|
QString buttonstyle = QString("QPushButton { border: none; background-color: %1; }").arg(CCHROME);
|
|
back->setStyleSheet(buttonstyle);
|
|
forward->setStyleSheet(buttonstyle);
|
|
|
|
// All platforms
|
|
QPalette tabbarPalette;
|
|
tabbar->setAutoFillBackground(true);
|
|
tabbar->setShape(QTabBar::RoundedSouth);
|
|
tabbar->setDrawBase(false);
|
|
|
|
tabbarPalette.setBrush(backgroundRole(), GColor(CCHROME));
|
|
tabbarPalette.setBrush(foregroundRole(), GCColor::invertColor(GColor(CCHROME)));
|
|
tabbar->setPalette(tabbarPalette);
|
|
athleteView->setPalette(tabbarPalette);
|
|
|
|
head->updateGeometry();
|
|
repaint();
|
|
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Measures
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::setMeasuresMenu()
|
|
{
|
|
measuresMenu->clear();
|
|
if (currentTab->context->athlete == nullptr) return;
|
|
Measures *measures = currentTab->context->athlete->measures;
|
|
int group = 0;
|
|
foreach(QString name, measures->getGroupNames()) {
|
|
|
|
// we use the group index to identify the measures group
|
|
QAction *service = new QAction(NULL);
|
|
service->setText(name);
|
|
service->setData(group++);
|
|
measuresMenu->addAction(service);
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::downloadMeasures(QAction *action)
|
|
{
|
|
// download or import from CSV file
|
|
if (currentTab->context->athlete == nullptr) return;
|
|
Measures *measures = currentTab->context->athlete->measures;
|
|
int group = action->data().toInt();
|
|
MeasuresDownload dialog(currentTab->context, measures->getGroup(group));
|
|
dialog.exec();
|
|
}
|
|
|
|
void
|
|
MainWindow::actionClicked(int index)
|
|
{
|
|
switch(index) {
|
|
|
|
default:
|
|
case 0: currentTab->addIntervals();
|
|
break;
|
|
|
|
case 1 : splitRide();
|
|
break;
|
|
|
|
case 2 : deleteRide();
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::addIntervals()
|
|
{
|
|
currentTab->addIntervals();
|
|
}
|
|
|
|
void
|
|
MainWindow::ridesAutoImport() {
|
|
|
|
currentTab->context->athlete->importFilesWhenOpeningAthlete();
|
|
|
|
}
|
|
|
|
#ifdef GC_WANT_PYTHON
|
|
void MainWindow::onEditMenuAboutToShow()
|
|
{
|
|
bool embedPython = appsettings->value(nullptr, GC_EMBED_PYTHON, true).toBool();
|
|
pyFixesMenu->menuAction()->setVisible(embedPython);
|
|
}
|
|
|
|
void MainWindow::buildPyFixesMenu()
|
|
{
|
|
pyFixesMenu->clear();
|
|
|
|
QList<FixPyScript *> fixPyScripts = fixPySettings->getScripts();
|
|
foreach (FixPyScript *fixPyScript, fixPyScripts) {
|
|
QAction *action = new QAction(QString("%1...").arg(fixPyScript->name), this);
|
|
pyFixesMenu->addAction(action);
|
|
connect(action, SIGNAL(triggered()), toolMapper, SLOT(map()));
|
|
toolMapper->setMapping(action, "_fix_py_" + fixPyScript->name);
|
|
}
|
|
|
|
pyFixesMenu->addSeparator();
|
|
pyFixesMenu->addAction(tr("New Python Fix..."), this, SLOT (showCreateFixPyScriptDlg()));
|
|
pyFixesMenu->addAction(tr("Manage Python Fixes..."), this, SLOT (showManageFixPyScriptsDlg()));
|
|
}
|
|
|
|
void MainWindow::showManageFixPyScriptsDlg() {
|
|
ManageFixPyScriptsDialog dlg(currentTab->context);
|
|
dlg.exec();
|
|
}
|
|
|
|
void MainWindow::showCreateFixPyScriptDlg() {
|
|
EditFixPyScriptDialog dlg(currentTab->context, nullptr, this);
|
|
dlg.exec();
|
|
}
|
|
#endif
|
|
|
|
#ifdef GC_HAS_CLOUD_DB
|
|
void
|
|
MainWindow::cloudDBuserEditChart()
|
|
{
|
|
if (!(appsettings->cvalue(currentTab->context->athlete->cyclist, GC_CLOUDDB_TC_ACCEPTANCE, false).toBool())) {
|
|
CloudDBAcceptConditionsDialog acceptDialog(currentTab->context->athlete->cyclist);
|
|
acceptDialog.setModal(true);
|
|
if (acceptDialog.exec() == QDialog::Rejected) {
|
|
return;
|
|
};
|
|
}
|
|
|
|
if (currentTab->context->cdbChartListDialog == NULL) {
|
|
currentTab->context->cdbChartListDialog = new CloudDBChartListDialog();
|
|
}
|
|
|
|
// force refresh in prepare to allways get the latest data here
|
|
if (currentTab->context->cdbChartListDialog->prepareData(currentTab->context->athlete->cyclist, CloudDBCommon::UserEdit)) {
|
|
currentTab->context->cdbChartListDialog->exec(); // no action when closed
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::cloudDBuserEditUserMetric()
|
|
{
|
|
if (!(appsettings->cvalue(currentTab->context->athlete->cyclist, GC_CLOUDDB_TC_ACCEPTANCE, false).toBool())) {
|
|
CloudDBAcceptConditionsDialog acceptDialog(currentTab->context->athlete->cyclist);
|
|
acceptDialog.setModal(true);
|
|
if (acceptDialog.exec() == QDialog::Rejected) {
|
|
return;
|
|
};
|
|
}
|
|
|
|
if (currentTab->context->cdbUserMetricListDialog == NULL) {
|
|
currentTab->context->cdbUserMetricListDialog = new CloudDBUserMetricListDialog();
|
|
}
|
|
|
|
// force refresh in prepare to allways get the latest data here
|
|
if (currentTab->context->cdbUserMetricListDialog->prepareData(currentTab->context->athlete->cyclist, CloudDBCommon::UserEdit)) {
|
|
currentTab->context->cdbUserMetricListDialog->exec(); // no action when closed
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::cloudDBcuratorEditChart()
|
|
{
|
|
// first check if the user is a curator
|
|
CloudDBCuratorClient *curatorClient = new CloudDBCuratorClient;
|
|
if (curatorClient->isCurator(appsettings->cvalue(currentTab->context->athlete->cyclist, GC_ATHLETE_ID, "" ).toString())) {
|
|
|
|
if (currentTab->context->cdbChartListDialog == NULL) {
|
|
currentTab->context->cdbChartListDialog = new CloudDBChartListDialog();
|
|
}
|
|
|
|
// force refresh in prepare to allways get the latest data here
|
|
if (currentTab->context->cdbChartListDialog->prepareData(currentTab->context->athlete->cyclist, CloudDBCommon::CuratorEdit)) {
|
|
currentTab->context->cdbChartListDialog->exec(); // no action when closed
|
|
}
|
|
} else {
|
|
QMessageBox::warning(0, tr("CloudDB"), QString(tr("Current athlete is not registered as curator - please contact the GoldenCheetah team")));
|
|
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::cloudDBcuratorEditUserMetric()
|
|
{
|
|
// first check if the user is a curator
|
|
CloudDBCuratorClient *curatorClient = new CloudDBCuratorClient;
|
|
if (curatorClient->isCurator(appsettings->cvalue(currentTab->context->athlete->cyclist, GC_ATHLETE_ID, "" ).toString())) {
|
|
|
|
if (currentTab->context->cdbUserMetricListDialog == NULL) {
|
|
currentTab->context->cdbUserMetricListDialog = new CloudDBUserMetricListDialog();
|
|
}
|
|
|
|
// force refresh in prepare to allways get the latest data here
|
|
if (currentTab->context->cdbUserMetricListDialog->prepareData(currentTab->context->athlete->cyclist, CloudDBCommon::CuratorEdit)) {
|
|
currentTab->context->cdbUserMetricListDialog->exec(); // no action when closed
|
|
}
|
|
} else {
|
|
QMessageBox::warning(0, tr("CloudDB"), QString(tr("Current athlete is not registered as curator - please contact the GoldenCheetah team")));
|
|
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::cloudDBshowStatus() {
|
|
|
|
CloudDBStatusClient::displayCloudDBStatus();
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
void
|
|
MainWindow::setUploadMenu()
|
|
{
|
|
uploadMenu->clear();
|
|
foreach(QString name, CloudServiceFactory::instance().serviceNames()) {
|
|
|
|
const CloudService *s = CloudServiceFactory::instance().service(name);
|
|
if (!s || appsettings->cvalue(currentTab->context->athlete->cyclist, s->activeSettingName(), "false").toString() != "true") continue;
|
|
|
|
if (s->capabilities() & CloudService::Upload) {
|
|
|
|
// we need the technical name to identify the service to be called
|
|
QAction *service = new QAction(NULL);
|
|
service->setText(s->uiName());
|
|
service->setData(name);
|
|
|
|
// Kent doesn't use the standard uploader, we trap for that
|
|
// in the upload action method
|
|
uploadMenu->addAction(service);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::setSyncMenu()
|
|
{
|
|
syncMenu->clear();
|
|
foreach(QString name, CloudServiceFactory::instance().serviceNames()) {
|
|
|
|
const CloudService *s = CloudServiceFactory::instance().service(name);
|
|
if (!s || appsettings->cvalue(currentTab->context->athlete->cyclist, s->activeSettingName(), "false").toString() != "true") continue;
|
|
|
|
if (s->capabilities() & CloudService::Query) {
|
|
|
|
// We don't sync with Kent
|
|
if (s->id() == "University of Kent") continue;
|
|
|
|
// we need the technical name to identify the service to be called
|
|
QAction *service = new QAction(NULL);
|
|
service->setText(s->uiName());
|
|
service->setData(name);
|
|
syncMenu->addAction(service);
|
|
}
|
|
}
|
|
}
|
|
|
|
|