mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
* Assigned keyboard shortcuts to toggle various sidebars and panels * Swapped Trends and Activities in the View-menu to match the order in the view sidebar
2775 lines
93 KiB
C++
2775 lines
93 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 <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 "TrainerDay.h"
|
|
#include "StravaRoutesDownload.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 "AthleteConfigDialog.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 "BatchProcessingDialog.h"
|
|
#include "MeasuresDownload.h"
|
|
#include "WorkoutWizard.h"
|
|
#include "TrainerDayDownloadDialog.h"
|
|
#include "AddDeviceWizard.h"
|
|
#include "Dropbox.h"
|
|
#include "SixCycle.h"
|
|
#include "OpenData.h"
|
|
#include "AddCloudWizard.h"
|
|
#include "LocalFileStore.h"
|
|
#include "CloudService.h"
|
|
|
|
// GUI Widgets
|
|
#include "AthleteTab.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
|
|
|
|
// 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 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
|
|
|
|
// constants for gui
|
|
static int gl_toolheight=28;
|
|
|
|
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();
|
|
|
|
#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);
|
|
currentAthleteTab = new AthleteTab(context);
|
|
|
|
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());
|
|
}
|
|
|
|
#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
|
|
*--------------------------------------------------------------------*/
|
|
splash->showMessage(tr("Setting up GUI..."));
|
|
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 {
|
|
|
|
AppearanceSettings defaults = GSettings::defaultAppearanceSettings();
|
|
|
|
// center on the available screen (minus toolbar/sidebar)
|
|
move(defaults.windowsize.x(), defaults.windowsize.y());
|
|
resize(defaults.windowsize.width(), defaults.windowsize.height());
|
|
|
|
}
|
|
|
|
// store "last_openend" athlete for next time
|
|
appsettings->setValue(GC_SETTINGS_LAST, context->athlete->home->root().dirName());
|
|
|
|
/*----------------------------------------------------------------------
|
|
* ScopeBar as sidebar from v3.6
|
|
*--------------------------------------------------------------------*/
|
|
splash->showMessage(tr("Setting up GUI: Scopebar..."));
|
|
|
|
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
|
|
*--------------------------------------------------------------------*/
|
|
splash->showMessage(tr("Setting up GUI: 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
|
|
*--------------------------------------------------------------------*/
|
|
splash->showMessage(tr("Setting up GUI: Toolbar..."));
|
|
head = new GcToolBar(this);
|
|
|
|
QStyle *toolStyle = QStyleFactory::create("fusion");
|
|
|
|
// get those icons
|
|
sidebarIcon = iconFromPNG(":images/titlebar/sidebar.png");
|
|
lowbarIcon = iconFromPNG(":images/titlebar/bottombar.png");
|
|
tiledIcon = iconFromPNG(":images/titlebar/tile.png");
|
|
backIcon = iconFromPNG(":images/mac/back.png");
|
|
whatIcon = iconFromPNG(":images/titlebar/whatsthis.png");
|
|
forwardIcon = iconFromPNG(":images/mac/forward.png");
|
|
QSize isize(16 *dpiXFactor,16 *dpiYFactor);
|
|
|
|
back = new QPushButton(this);
|
|
back->setIcon(backIcon);
|
|
back->setFixedHeight(gl_toolheight *dpiYFactor);
|
|
back->setFixedWidth(gl_toolheight *dpiYFactor);
|
|
back->setIconSize(isize);
|
|
back->setStyle(toolStyle);
|
|
connect(back, SIGNAL(clicked(bool)), this, SIGNAL(backClicked()));
|
|
HelpWhatsThis *helpBack = new HelpWhatsThis(back);
|
|
back->setWhatsThis(helpBack->getWhatsThisText(HelpWhatsThis::ToolBar_Back));
|
|
|
|
forward = new QPushButton(this);
|
|
forward->setIcon(forwardIcon);
|
|
forward->setFixedHeight(gl_toolheight *dpiYFactor);
|
|
forward->setFixedWidth(gl_toolheight *dpiYFactor);
|
|
forward->setIconSize(isize);
|
|
forward->setStyle(toolStyle);
|
|
connect(forward, SIGNAL(clicked(bool)), this, SIGNAL(forwardClicked()));
|
|
HelpWhatsThis *helpForward = new HelpWhatsThis(forward);
|
|
forward->setWhatsThis(helpForward->getWhatsThisText(HelpWhatsThis::ToolBar_Forward));
|
|
|
|
lowbar = new QPushButton(this);
|
|
lowbar->setIcon(lowbarIcon);
|
|
lowbar->setFixedHeight(gl_toolheight *dpiYFactor);
|
|
lowbar->setFixedWidth(gl_toolheight *dpiYFactor);
|
|
lowbar->setIconSize(isize);
|
|
lowbar->setStyle(toolStyle);
|
|
lowbar->setToolTip(tr("Toggle Compare Pane"));
|
|
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(gl_toolheight * dpiYFactor);
|
|
sidelist->setFixedWidth(gl_toolheight *dpiYFactor);
|
|
sidelist->setIconSize(isize);
|
|
sidelist->setStyle(toolStyle);
|
|
sidelist->setToolTip(tr("Toggle Sidebar"));
|
|
connect(sidelist, SIGNAL(clicked(bool)), this, SLOT(toggleSidebar()));
|
|
HelpWhatsThis *helpSideBar = new HelpWhatsThis(sidelist);
|
|
sidelist->setWhatsThis(helpSideBar->getWhatsThisText(HelpWhatsThis::ToolBar_ToggleSidebar));
|
|
|
|
tabtile = new QPushButton(this);
|
|
tabtile->setIcon(tiledIcon);
|
|
tabtile->setFixedHeight(gl_toolheight *dpiYFactor);
|
|
tabtile->setFixedWidth(gl_toolheight *dpiYFactor);
|
|
tabtile->setIconSize(isize);
|
|
lowbar->setStyle(toolStyle);
|
|
tabtile->setToolTip(tr("Toggle Tab/Tile"));
|
|
connect(tabtile, SIGNAL(clicked(bool)), this, SLOT(toggleStyle()));
|
|
HelpWhatsThis *helpTabtile = new HelpWhatsThis(tabtile);
|
|
tabtile->setWhatsThis(helpTabtile->getWhatsThisText(HelpWhatsThis::ToolBar_TabTile));
|
|
|
|
// What's this button
|
|
whatsthis = new QPushButton(this);
|
|
whatsthis->setIcon(whatIcon);
|
|
whatsthis->setFixedHeight(gl_toolheight * dpiYFactor);
|
|
whatsthis->setFixedWidth(gl_toolheight * dpiYFactor);
|
|
whatsthis->setIconSize(isize);
|
|
whatsthis->setStyle(toolStyle);
|
|
whatsthis->setToolTip(tr("What's This?"));
|
|
connect(whatsthis, SIGNAL(clicked(bool)), this, SLOT(enterWhatsThisMode()));
|
|
|
|
// 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(gl_toolheight * dpiYFactor);
|
|
connect(perspectiveSelector, SIGNAL(currentIndexChanged(int)), this, SLOT(perspectiveSelected(int)));
|
|
HelpWhatsThis *helpPerspectiveSelector = new HelpWhatsThis(perspectiveSelector);
|
|
perspectiveSelector->setWhatsThis(helpPerspectiveSelector->getWhatsThisText(HelpWhatsThis::ToolBar_PerspectiveSelector));
|
|
|
|
searchBox = new SearchFilterBox(this,context,false);
|
|
|
|
searchBox->setStyle(toolStyle);
|
|
searchBox->setFixedWidth(400 * dpiXFactor);
|
|
searchBox->setFixedHeight(gl_toolheight * 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(tabtile);
|
|
#ifdef Q_OS_MAC // no menu on mac, so lets have some breathing space
|
|
head->setFixedHeight(searchBox->height() + (20 *dpiXFactor * 2));
|
|
#else
|
|
head->setFixedHeight(searchBox->height() + (16 *dpiXFactor));
|
|
#endif
|
|
|
|
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);
|
|
head->addWidget(whatsthis);
|
|
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
|
|
*--------------------------------------------------------------------*/
|
|
splash->showMessage(tr("Setting up GUI: 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
|
|
athletetabs.insert(currentAthleteTab->context->athlete->home->root().dirName(), currentAthleteTab);
|
|
|
|
// stack, list and bar all share a common index
|
|
tabList.append(currentAthleteTab);
|
|
tabbar->addTab(currentAthleteTab->context->athlete->home->root().dirName());
|
|
tabStack->addWidget(currentAthleteTab);
|
|
tabStack->setCurrentIndex(0);
|
|
|
|
connect(tabbar, SIGNAL(dragTab(int)), this, SLOT(switchAthleteTab(int)));
|
|
connect(tabbar, SIGNAL(currentChanged(int)), this, SLOT(switchAthleteTab(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);
|
|
|
|
#if QT_VERSION >= 0x060000
|
|
/*----------------------------------------------------------------------
|
|
* Hack to avoid a flickering MainWindow when showing a QWebEngineView in a chart, e.g. a Map:
|
|
* Temporarily add a dummy QWebEngineView with some random content before the MainWindow is shown
|
|
* https://forum.qt.io/topic/141398/qwebengineview-closes-reopens-window-when-added-dynamically
|
|
*--------------------------------------------------------------------*/
|
|
QWebEngineView *dummywev = new QWebEngineView();
|
|
dummywev->page()->setHtml("<html></html>");
|
|
mainLayout->addWidget(dummywev);
|
|
mainLayout->removeWidget(dummywev);
|
|
delete dummywev;
|
|
#endif
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Application Menus
|
|
*--------------------------------------------------------------------*/
|
|
splash->showMessage(tr("Setting up GUI: Application Menus..."));
|
|
#ifdef WIN32
|
|
QString menuColorString = GColor(CTOOLBAR).name();
|
|
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, &QSignalMapper::mappedString, this, &MainWindow::openAthleteTab);
|
|
|
|
fileMenu->addSeparator();
|
|
backupAthleteMenu = fileMenu->addMenu(tr("Backup..."));
|
|
connect(backupAthleteMenu, SIGNAL(aboutToShow()), this, SLOT(setBackupAthleteMenu()));
|
|
backupMapper = new QSignalMapper(this); // maps each option
|
|
connect(backupMapper, &QSignalMapper::mappedString, this, &MainWindow::backupAthlete);
|
|
|
|
fileMenu->addSeparator();
|
|
deleteAthleteMenu = fileMenu->addMenu(tr("Delete..."));
|
|
connect(deleteAthleteMenu, SIGNAL(aboutToShow()), this, SLOT(setDeleteAthleteMenu()));
|
|
deleteMapper = new QSignalMapper(this); // maps each option
|
|
connect(deleteMapper, &QSignalMapper::mappedString, this, &MainWindow::deleteAthlete);
|
|
|
|
fileMenu->addSeparator();
|
|
fileMenu->addAction(tr("Settings..."), this, SLOT(athleteSettings()));
|
|
fileMenu->addSeparator();
|
|
fileMenu->addAction(tr("Save all modified activities"), this, SLOT(saveAllUnsavedRides()));
|
|
fileMenu->addSeparator();
|
|
QAction *actionQuit = new QAction(tr("&Quit"), fileMenu);
|
|
actionQuit->setShortcuts(QKeySequence::Quit);
|
|
actionQuit->setShortcutContext(Qt::ApplicationShortcut);
|
|
connect(actionQuit, SIGNAL(triggered()), this, SLOT(closeWindow()));
|
|
fileMenu->addAction(actionQuit);
|
|
//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()), QKeySequence("Ctrl+D"));
|
|
rideMenu->addAction(tr("&Import from file..."), this, SLOT (importFile()), QKeySequence("Ctrl+I"));
|
|
rideMenu->addAction(tr("&Manual entry..."), this, SLOT(manualRide()), QKeySequence("Ctrl+M"));
|
|
rideMenu->addSeparator ();
|
|
rideMenu->addAction(tr("&Export..."), this, SLOT(exportRide()), QKeySequence("Ctrl+E"));
|
|
rideMenu->addAction(tr("&Batch Processing..."), this, SLOT(batchProcessing()), QKeySequence("Ctrl+B"));
|
|
|
|
rideMenu->addSeparator ();
|
|
rideMenu->addAction(tr("&Save activity"), this, SLOT(saveRide()), QKeySequence("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()), QKeySequence(""));
|
|
|
|
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(QKeySequence("")); // Ctrl+A is already in use for select all
|
|
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(QKeySequence("")); // Ctrl+C is already in use for clipboard copy
|
|
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 TrainerDay..."), this, SLOT(downloadTrainerDay()));
|
|
optionsMenu->addAction(tr("Download workouts from Strava Routes..."), this, SLOT(downloadStravaRoutes()));
|
|
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()));
|
|
optionsMenu->addAction(tr("Export Metrics as CSV..."), this, SLOT(exportMetrics()));
|
|
|
|
#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));
|
|
|
|
|
|
editMenu = menuBar()->addMenu(tr("&Edit"));
|
|
connect(editMenu, SIGNAL(aboutToShow()), this, SLOT(onEditMenuAboutToShow()));
|
|
|
|
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
|
|
showhideViewbar = viewMenu->addAction(tr("Show View Sidebar"), this, SLOT(showViewbar(bool)), QKeySequence("F2"));
|
|
showhideViewbar->setCheckable(true);
|
|
showhideViewbar->setChecked(true);
|
|
showhideSidebar = viewMenu->addAction(tr("Show Left Sidebar"), this, SLOT(showSidebar(bool)), QKeySequence("F3"));
|
|
showhideSidebar->setCheckable(true);
|
|
showhideSidebar->setChecked(true);
|
|
showhideLowbar = viewMenu->addAction(tr("Show Compare Pane"), this, SLOT(showLowbar(bool)), QKeySequence("F4"));
|
|
showhideLowbar->setCheckable(true);
|
|
showhideLowbar->setChecked(false);
|
|
showhideToolbar = viewMenu->addAction(tr("Show Toolbar"), this, SLOT(showToolbar(bool)), QKeySequence("F5"));
|
|
showhideToolbar->setCheckable(true);
|
|
showhideToolbar->setChecked(true);
|
|
showhideTabbar = viewMenu->addAction(tr("Show Athlete Tabs"), this, SLOT(showTabbar(bool)), QKeySequence("F6"));
|
|
showhideTabbar->setCheckable(true);
|
|
showhideTabbar->setChecked(true);
|
|
|
|
viewMenu->addSeparator();
|
|
viewMenu->addAction(tr("Trends"), this, SLOT(selectTrends()));
|
|
viewMenu->addAction(tr("Activities"), this, SLOT(selectAnalysis()));
|
|
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
|
|
*--------------------------------------------------------------------*/
|
|
splash->showMessage(tr("Selecting ride..."));
|
|
|
|
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(currentAthleteTab->context); // set to whatever we started with
|
|
selectAnalysis();
|
|
|
|
//grab focus
|
|
currentAthleteTab->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)
|
|
splash->showMessage(tr("Checking for udates..."));
|
|
OpenData::check(currentAthleteTab->context);
|
|
#else
|
|
fprintf(stderr, "OpenData disabled, secret not defined.\n"); fflush(stderr);
|
|
#endif
|
|
|
|
#ifdef GC_HAS_CLOUD_DB
|
|
splash->showMessage(tr("Asking for telemetry..."));
|
|
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
|
|
|
|
// get rid of splash when currentTab is shown
|
|
clearSplash();
|
|
}
|
|
|
|
|
|
/*----------------------------------------------------------------------
|
|
* GUI
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::setSplash()
|
|
{
|
|
QString versionText = QString(tr("%1 - build %2")).arg(VERSION_STRING).arg(VERSION_LATEST);
|
|
splash = new SplashScreen(":images/splashscreen.png", versionText, 533, 100);
|
|
}
|
|
|
|
void
|
|
MainWindow::clearSplash()
|
|
{
|
|
delete splash;
|
|
splash = nullptr;
|
|
}
|
|
|
|
void
|
|
MainWindow::toggleSidebar()
|
|
{
|
|
currentAthleteTab->toggleSidebar();
|
|
setToolButtons();
|
|
}
|
|
void
|
|
MainWindow::showViewbar(bool want)
|
|
{
|
|
want ? sidebar->show() : sidebar->hide();
|
|
showhideViewbar->setChecked(want);
|
|
setToolButtons();
|
|
}
|
|
void
|
|
MainWindow::showSidebar(bool want)
|
|
{
|
|
currentAthleteTab->setSidebarEnabled(want);
|
|
showhideSidebar->setChecked(want);
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::toggleLowbar()
|
|
{
|
|
if (currentAthleteTab->hasBottom()) currentAthleteTab->setBottomRequested(!currentAthleteTab->isBottomRequested());
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::showLowbar(bool want)
|
|
{
|
|
if (currentAthleteTab->hasBottom()) currentAthleteTab->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(currentAthleteTab->currentView()) {
|
|
case 0 : mask = VIEW_TRENDS; break;
|
|
default:
|
|
case 1 : mask = VIEW_ANALYSIS; break;
|
|
case 2 : mask = VIEW_DIARY; break;
|
|
case 3 : mask = VIEW_TRAIN; 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(currentAthleteTab->currentView()) {
|
|
case 0 : mask = VIEW_TRENDS; break;
|
|
default:
|
|
case 1 : mask = VIEW_ANALYSIS; break;
|
|
case 2 : mask = VIEW_DIARY; break;
|
|
case 3 : mask = VIEW_TRAIN; 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)
|
|
currentAthleteTab->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 = currentAthleteTab->currentView();
|
|
AbstractView *current = NULL;
|
|
|
|
QString typedesc;
|
|
|
|
switch (view) {
|
|
case 0: current = currentAthleteTab->homeView; typedesc = "Trends"; break;
|
|
case 1: current = currentAthleteTab->analysisView; typedesc = "Analysis"; break;
|
|
case 2: current = currentAthleteTab->diaryView; typedesc = "Diary"; break;
|
|
case 3: current = currentAthleteTab->trainView; typedesc = "Train"; break;
|
|
}
|
|
|
|
// export the current perspective to a file
|
|
QString suffix;
|
|
QString fileName = QFileDialog::getSaveFileName(this, tr("Export Persepctive"),
|
|
QDir::homePath()+"/"+ typedesc + " " + 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 = currentAthleteTab->currentView();
|
|
AbstractView *current = NULL;
|
|
|
|
switch (view) {
|
|
case 0: current = currentAthleteTab->homeView; break;
|
|
case 1: current = currentAthleteTab->analysisView; break;
|
|
case 2: current = currentAthleteTab->diaryView; break;
|
|
case 3: current = currentAthleteTab->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;
|
|
if (current->importPerspective(fileName)) {
|
|
|
|
// on success we select the new one
|
|
resetPerspective(view);
|
|
//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=currentAthleteTab->view(currentAthleteTab->currentView())->page();
|
|
if (page->currentStyle == 0 && page->currentChart())
|
|
page->currentChart()->exportChartToCloudDB();
|
|
}
|
|
|
|
void
|
|
MainWindow::addChartFromCloudDB()
|
|
{
|
|
if (!(appsettings->cvalue(currentAthleteTab->context->athlete->cyclist, GC_CLOUDDB_TC_ACCEPTANCE, false).toBool())) {
|
|
CloudDBAcceptConditionsDialog acceptDialog(currentAthleteTab->context->athlete->cyclist);
|
|
acceptDialog.setModal(true);
|
|
if (acceptDialog.exec() == QDialog::Rejected) {
|
|
return;
|
|
};
|
|
}
|
|
|
|
if (currentAthleteTab->context->cdbChartListDialog == NULL) {
|
|
currentAthleteTab->context->cdbChartListDialog = new CloudDBChartListDialog();
|
|
}
|
|
|
|
if (currentAthleteTab->context->cdbChartListDialog->prepareData(currentAthleteTab->context->athlete->cyclist, CloudDBCommon::UserImport, currentAthleteTab->currentView())) {
|
|
if (currentAthleteTab->context->cdbChartListDialog->exec() == QDialog::Accepted) {
|
|
|
|
// get selected chartDef
|
|
QList<QString> chartDefs = currentAthleteTab->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++) {
|
|
currentAthleteTab->context->mainWindow->athleteTab()->view(currentAthleteTab->currentView())->importChart(properties.at(i), false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void
|
|
MainWindow::toggleStyle()
|
|
{
|
|
currentAthleteTab->toggleTile();
|
|
styleAction->setChecked(currentAthleteTab->isTiled());
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::toggleFullScreen()
|
|
{
|
|
#ifdef Q_OS_MAC
|
|
QRect screenSize = QGuiApplication::primaryScreen()->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) {
|
|
|
|
// if we are entering full screen mode we hide the sidebar
|
|
if (windowState()&Qt::WindowFullScreen) showViewbar(false);
|
|
else showViewbar(true);
|
|
|
|
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(currentAthleteTab->context->athlete->home->root(), currentAthleteTab->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)
|
|
{
|
|
QGuiApplication::setOverrideCursor(Qt::WaitCursor);
|
|
QList<AthleteTab*> closing = tabList;
|
|
bool needtosave = false;
|
|
bool importrunning = false;
|
|
|
|
// close all the tabs .. if any refuse we need to ignore
|
|
// the close event
|
|
foreach(AthleteTab *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;
|
|
QGuiApplication::restoreOverrideCursor();
|
|
QMessageBox::information(this, tr("Activity Import"), tr("Closing of athlete window not possible while background activity import is in progress..."));
|
|
QGuiApplication::setOverrideCursor(Qt::WaitCursor);
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
removeAthleteTab(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());
|
|
QGuiApplication::restoreOverrideCursor();
|
|
}
|
|
|
|
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) { currentAthleteTab->context->setFilter(f); }
|
|
void MainWindow::clearFilter() { currentAthleteTab->context->clearFilter(); }
|
|
|
|
void
|
|
MainWindow::aboutDialog()
|
|
{
|
|
AboutDialog *ad = new AboutDialog(currentAthleteTab->context);
|
|
ad->exec();
|
|
}
|
|
|
|
void MainWindow::showSolveCP()
|
|
{
|
|
SolveCPDialog *td = new SolveCPDialog(this, currentAthleteTab->context);
|
|
td->show();
|
|
}
|
|
|
|
void MainWindow::showEstimateCP()
|
|
{
|
|
EstimateCPDialog *td = new EstimateCPDialog();
|
|
td->show();
|
|
}
|
|
|
|
void MainWindow::showRhoEstimator()
|
|
{
|
|
ToolsRhoEstimator *tre = new ToolsRhoEstimator(currentAthleteTab->context);
|
|
tre->show();
|
|
}
|
|
|
|
void MainWindow::showVDOTCalculator()
|
|
{
|
|
VDOTCalculator *VDOTcalculator = new VDOTCalculator();
|
|
VDOTcalculator->show();
|
|
}
|
|
|
|
void MainWindow::showWorkoutWizard()
|
|
{
|
|
WorkoutWizard *ww = new WorkoutWizard(currentAthleteTab->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))
|
|
currentAthleteTab->resetLayout(perspectiveSelector);
|
|
}
|
|
|
|
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*)currentAthleteTab->context->currentRideItem();
|
|
if (rideitem) {
|
|
ManualDataProcessorDialog *p = new ManualDataProcessorDialog(currentAthleteTab->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(currentAthleteTab->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: selectTrends(); 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()
|
|
{
|
|
resetPerspective(1);
|
|
//currentTab->analysisView->setPerspectives(perspectiveSelector);
|
|
viewStack->setCurrentIndex(1);
|
|
sidebar->setItemSelected(3, true);
|
|
currentAthleteTab->selectView(1);
|
|
perspectiveSelector->show();
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::selectTrain()
|
|
{
|
|
resetPerspective(3);
|
|
//currentTab->trainView->setPerspectives(perspectiveSelector);
|
|
viewStack->setCurrentIndex(1);
|
|
sidebar->setItemSelected(5, true);
|
|
currentAthleteTab->selectView(3);
|
|
perspectiveSelector->show();
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::selectDiary()
|
|
{
|
|
resetPerspective(2);
|
|
//currentTab->diaryView->setPerspectives(perspectiveSelector);
|
|
viewStack->setCurrentIndex(1);
|
|
currentAthleteTab->selectView(2);
|
|
perspectiveSelector->show();
|
|
setToolButtons();
|
|
}
|
|
|
|
void
|
|
MainWindow::selectTrends()
|
|
{
|
|
resetPerspective(0);
|
|
//currentTab->homeView->setPerspectives(perspectiveSelector);
|
|
viewStack->setCurrentIndex(1);
|
|
sidebar->setItemSelected(2, true);
|
|
currentAthleteTab->selectView(0);
|
|
perspectiveSelector->show();
|
|
setToolButtons();
|
|
}
|
|
|
|
|
|
bool
|
|
MainWindow::isStarting
|
|
() const
|
|
{
|
|
return splash != nullptr;
|
|
}
|
|
|
|
|
|
void
|
|
MainWindow::setToolButtons()
|
|
{
|
|
int select = currentAthleteTab->isTiled() ? 1 : 0;
|
|
int lowselected = currentAthleteTab->isBottomRequested() ? 1 : 0;
|
|
|
|
styleAction->setChecked(select);
|
|
showhideLowbar->setChecked(lowselected);
|
|
|
|
//if (styleSelector->isSegmentSelected(select) == false)
|
|
//styleSelector->setSegmentSelected(select, true);
|
|
|
|
int index = currentAthleteTab->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::switchPerspective(int index)
|
|
{
|
|
if (pactive) return;
|
|
|
|
if (index >=0 && index < perspectiveSelector->count())
|
|
perspectiveSelector->setCurrentIndex(index);
|
|
}
|
|
|
|
void
|
|
MainWindow::resetPerspective(int view, bool force)
|
|
{
|
|
static AthleteTab *lastathlete=NULL;
|
|
static int lastview=-1;
|
|
|
|
if (!force && lastview == view && lastathlete == currentAthleteTab) return;
|
|
|
|
// remember who last updated it.
|
|
lastathlete = currentAthleteTab;
|
|
lastview = view;
|
|
|
|
// don't argue just reset the perspective for this view
|
|
AbstractView *current = NULL;
|
|
switch (view) {
|
|
|
|
case 0: current = currentAthleteTab->homeView; break;
|
|
case 1: current = currentAthleteTab->analysisView; break;
|
|
case 2: current = currentAthleteTab->diaryView; break;
|
|
case 3: current = currentAthleteTab->trainView; break;
|
|
}
|
|
|
|
// set the perspective
|
|
pactive=true;
|
|
current->setPerspectives(perspectiveSelector);
|
|
perspectiveSelector->setCurrentIndex(current->currentPerspective());
|
|
pactive=false;
|
|
}
|
|
|
|
void
|
|
MainWindow::perspectiveSelected(int index)
|
|
{
|
|
if (pactive) return;
|
|
|
|
// set the perspective for the current view
|
|
int view = currentAthleteTab->currentView();
|
|
AbstractView *current = NULL;
|
|
switch (view) {
|
|
case 0: current = currentAthleteTab->homeView; break;
|
|
case 1: current = currentAthleteTab->analysisView; break;
|
|
case 2: current = currentAthleteTab->diaryView; break;
|
|
case 3: current = currentAthleteTab->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;
|
|
QString expression;
|
|
Perspective::switchenum trainswitch=Perspective::None;
|
|
AddPerspectiveDialog *dialog= new AddPerspectiveDialog(this, currentAthleteTab->context, name, expression, current->type, trainswitch);
|
|
int ret= dialog->exec();
|
|
delete dialog;
|
|
if (ret == QDialog::Accepted && name != "") {
|
|
|
|
// add...
|
|
Perspective *newone = current->addPerspective(name);
|
|
newone->setExpression(expression);
|
|
newone->setTrainSwitch(trainswitch);
|
|
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 = currentAthleteTab->currentView();
|
|
AbstractView *current = NULL;
|
|
|
|
switch (view) {
|
|
case 0: current = currentAthleteTab->homeView; break;
|
|
case 1: current = currentAthleteTab->analysisView; break;
|
|
case 2: current = currentAthleteTab->diaryView; break;
|
|
case 3: current = currentAthleteTab->trainView; break;
|
|
}
|
|
|
|
// which perspective is currently being selected (before we go setting the combobox)
|
|
Perspective *prior = current->perspective_;
|
|
|
|
// ok, so reset the combobox and force, since whilst it may have already
|
|
// been set for this athlete+view combination the config was just changed
|
|
// so it needs to be redone.
|
|
resetPerspective(view, true);
|
|
//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, images;
|
|
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();
|
|
|
|
} else if (Utils::isImage(filename)) {
|
|
images << filename;
|
|
|
|
// Look for Workout files only in Train view
|
|
} else if (currentAthleteTab->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
|
|
currentAthleteTab->context->athlete->presets += imported;
|
|
|
|
// notify we changed and tree updates
|
|
currentAthleteTab->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
|
|
selectTrends();
|
|
|
|
// now select what was added
|
|
currentAthleteTab->context->notifyPresetSelected(currentAthleteTab->context->athlete->presets.count()-1);
|
|
}
|
|
|
|
// are there any .gcharts to import?
|
|
if (list.count()) importCharts(list);
|
|
|
|
// import workouts
|
|
if (workouts.count()) Library::importFiles(currentAthleteTab->context, workouts, LibraryBatchImportConfirmation::forcedDialog);
|
|
|
|
// import images (these will be attached to the current ride)
|
|
if (images.count()) importImages(images);
|
|
|
|
// if there is anything left, process based upon view...
|
|
if (filenames.count()) {
|
|
|
|
// We have something to process then
|
|
RideImportWizard *dialog = new RideImportWizard (filenames, currentAthleteTab->context);
|
|
dialog->process(); // do it!
|
|
}
|
|
return;
|
|
}
|
|
|
|
void
|
|
MainWindow::importImages(QStringList list)
|
|
{
|
|
// we need to be on activities view and with a current
|
|
// ride otherwise we just ignore the list
|
|
if (currentAthleteTab->currentView() != 1 || currentAthleteTab->context->ride == NULL) {
|
|
QMessageBox::critical(this, tr("Import Images Failed"), tr("You can only import images on the activities view with an activity selected."));
|
|
return;
|
|
}
|
|
|
|
// lets import them
|
|
int count = currentAthleteTab->context->ride->importImages(list);
|
|
QMessageBox::information(this, tr("Import Images to Activity"), QString(tr("%1 images imported.")).arg(count));
|
|
}
|
|
|
|
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(currentAthleteTab->context, charts, this);
|
|
importer.exec();
|
|
}
|
|
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Ride Library Functions
|
|
*--------------------------------------------------------------------*/
|
|
|
|
|
|
void
|
|
MainWindow::downloadRide()
|
|
{
|
|
(new DownloadRideDialog(currentAthleteTab->context))->show();
|
|
}
|
|
|
|
|
|
void
|
|
MainWindow::manualRide()
|
|
{
|
|
(new ManualRideDialog(currentAthleteTab->context))->show();
|
|
}
|
|
|
|
void
|
|
MainWindow::batchProcessing()
|
|
{
|
|
BatchProcessingDialog *d = new BatchProcessingDialog(currentAthleteTab->context);
|
|
d->exec();
|
|
}
|
|
|
|
void
|
|
MainWindow::generateHeatMap()
|
|
{
|
|
GenerateHeatMapDialog *d = new GenerateHeatMapDialog(currentAthleteTab->context);
|
|
d->exec();
|
|
}
|
|
|
|
void
|
|
MainWindow::exportRide()
|
|
{
|
|
if (currentAthleteTab->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 = currentAthleteTab->context->ride ? currentAthleteTab->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(currentAthleteTab->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(currentAthleteTab->context, currentRide, file, getSuffix.cap(1));
|
|
|
|
} else if (idx==0){
|
|
|
|
CsvFileReader writer;
|
|
result = writer.writeRideFile(currentAthleteTab->context, currentRide, file, CsvFileReader::gc);
|
|
|
|
} else if (idx==1){
|
|
|
|
CsvFileReader writer;
|
|
result = writer.writeRideFile(currentAthleteTab->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(QRegularExpression("^"), "*.");
|
|
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, currentAthleteTab->context);
|
|
import->process();
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::saveRide()
|
|
{
|
|
// no ride
|
|
if (currentAthleteTab->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
|
|
currentAthleteTab->context->notifyMetadataFlush();
|
|
currentAthleteTab->context->ride->notifyRideMetadataChanged();
|
|
|
|
// nothing to do if not dirty
|
|
//XXX FORCE A SAVE if (currentTab->context->ride->isDirty() == false) return;
|
|
|
|
// save
|
|
if (currentAthleteTab->context->ride) {
|
|
saveRideSingleDialog(currentAthleteTab->context, currentAthleteTab->context->ride); // will signal save to everyone
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::athleteSettings()
|
|
{
|
|
AthleteConfigDialog *dialog = new AthleteConfigDialog(currentAthleteTab->context->athlete->home->root(), currentAthleteTab->context);
|
|
dialog->exec();
|
|
}
|
|
|
|
void
|
|
MainWindow::saveAllUnsavedRides()
|
|
{
|
|
// flush in-flight changes
|
|
currentAthleteTab->context->notifyMetadataFlush();
|
|
currentAthleteTab->context->ride->notifyRideMetadataChanged();
|
|
|
|
// save
|
|
if (currentAthleteTab->context->ride) {
|
|
saveAllFilesSilent(currentAthleteTab->context); // will signal save to everyone
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::revertRide()
|
|
{
|
|
currentAthleteTab->context->ride->close();
|
|
currentAthleteTab->context->ride->ride(); // force re-load
|
|
|
|
// in case reverted ride has different starttime
|
|
currentAthleteTab->context->ride->setStartTime(currentAthleteTab->context->ride->ride()->startTime());
|
|
currentAthleteTab->context->ride->ride()->emitReverted();
|
|
|
|
// and notify everyone we changed which also has the side
|
|
// effect of updating the cached values too
|
|
currentAthleteTab->context->notifyRideSelected(currentAthleteTab->context->ride);
|
|
}
|
|
|
|
void
|
|
MainWindow::splitRide()
|
|
{
|
|
if (currentAthleteTab->context->ride && currentAthleteTab->context->ride->ride() && currentAthleteTab->context->ride->ride()->dataPoints().count()) (new SplitActivityWizard(currentAthleteTab->context))->exec();
|
|
else {
|
|
if (!currentAthleteTab->context->ride || !currentAthleteTab->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 (currentAthleteTab->context->ride && currentAthleteTab->context->ride->ride() && currentAthleteTab->context->ride->ride()->dataPoints().count()) (new MergeActivityWizard(currentAthleteTab->context))->exec();
|
|
else {
|
|
if (!currentAthleteTab->context->ride || !currentAthleteTab->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 = currentAthleteTab->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)
|
|
currentAthleteTab->context->athlete->removeCurrentRide();
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Realtime Devices and Workouts
|
|
*--------------------------------------------------------------------*/
|
|
void
|
|
MainWindow::addDevice()
|
|
{
|
|
|
|
// lets get a new one
|
|
AddDeviceWizard *p = new AddDeviceWizard(currentAthleteTab->context);
|
|
p->show();
|
|
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Cyclists
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::newCyclistTab()
|
|
{
|
|
QDir newHome = currentAthleteTab->context->athlete->home->root();
|
|
newHome.cdUp();
|
|
QString name = ChooseCyclistDialog::newAthleteWizard(newHome);
|
|
if (!name.isEmpty()) {
|
|
emit newAthlete(name);
|
|
openAthleteTab(name);
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::closeWindow()
|
|
{
|
|
QGuiApplication::setOverrideCursor(Qt::WaitCursor);
|
|
// just call close, we might do more later
|
|
appsettings->syncQSettings();
|
|
QGuiApplication::restoreOverrideCursor();
|
|
close();
|
|
}
|
|
|
|
void
|
|
MainWindow::openAthleteTab(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
|
|
currentAthleteTab = new AthleteTab(context);
|
|
|
|
// clear splash - progress whilst loading tab
|
|
//clearSplash();
|
|
|
|
// first tab
|
|
athletetabs.insert(currentAthleteTab->context->athlete->home->root().dirName(), currentAthleteTab);
|
|
|
|
// stack, list and bar all share a common index
|
|
tabList.append(currentAthleteTab);
|
|
tabbar->addTab(currentAthleteTab->context->athlete->home->root().dirName());
|
|
tabStack->addWidget(currentAthleteTab);
|
|
|
|
// 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
|
|
currentAthleteTab->context->notifyLoadDone(name, context);
|
|
|
|
// now do the automatic ride file import
|
|
context->athlete->importFilesWhenOpeningAthlete();
|
|
}
|
|
|
|
void
|
|
MainWindow::closeTabClicked(int index)
|
|
{
|
|
|
|
AthleteTab *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
|
|
removeAthleteTab(tab);
|
|
}
|
|
|
|
bool
|
|
MainWindow::closeAthleteTab(QString name)
|
|
{
|
|
for(int i=0; i<tabbar->count(); i++) {
|
|
if (name == tabbar->tabText(i)) {
|
|
closeTabClicked(i);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MainWindow::closeAthleteTab()
|
|
{
|
|
// check for autoimport and let it finalize
|
|
if (currentAthleteTab->context->athlete->autoImport) {
|
|
if (currentAthleteTab->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(currentAthleteTab->context) == false) return false;
|
|
|
|
// if its the last tab we close the window
|
|
if (tabList.count() == 1)
|
|
closeWindow();
|
|
else {
|
|
removeAthleteTab(currentAthleteTab);
|
|
}
|
|
appsettings->syncQSettings();
|
|
// we did it
|
|
return true;
|
|
}
|
|
|
|
// no questions asked just wipe away the current tab
|
|
void
|
|
MainWindow::removeAthleteTab(AthleteTab *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) switchAthleteTab(index-1);
|
|
else switchAthleteTab(index+1);
|
|
}
|
|
|
|
// close gracefully
|
|
tab->close();
|
|
tab->context->athlete->close();
|
|
|
|
// remove from state
|
|
athletetabs.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
|
|
if (name.startsWith(".")) continue; // ignore dot folders
|
|
|
|
// 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, AthleteTab*> t(x->athletetabs);
|
|
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()), QKeySequence("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
|
|
if (name.startsWith(".")) continue; // ignore dot folders
|
|
|
|
// 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
|
|
if (name.startsWith(".")) continue; // ignore dot folders
|
|
|
|
// 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, AthleteTab*> t(x->athletetabs);
|
|
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)
|
|
{
|
|
if (viewStack->currentIndex() != 0) {
|
|
|
|
// not on athlete view...
|
|
resetPerspective(currentAthleteTab->currentView()); // will lazy load, hence doing it first
|
|
|
|
// restore window state from the supplied context
|
|
switch(currentAthleteTab->currentView()) {
|
|
case 0: sidebar->setItemSelected(2,true); break;
|
|
case 1: sidebar->setItemSelected(3,true); break;
|
|
case 2: break; // diary not an icon
|
|
case 3: sidebar->setItemSelected(5, true); break;
|
|
default: sidebar->setItemSelected(0, true); break;
|
|
}
|
|
}
|
|
|
|
showSidebar(context->showSidebar);
|
|
showToolbar(context->showToolbar);
|
|
//showTabbar(context->showTabbar);
|
|
showLowbar(context->showLowbar);
|
|
searchBox->setContext(context);
|
|
searchBox->setText(context->searchText);
|
|
}
|
|
|
|
void
|
|
MainWindow::switchAthleteTab(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(currentAthleteTab->context);
|
|
|
|
currentAthleteTab = tabList[index];
|
|
tabStack->setCurrentIndex(index);
|
|
|
|
// restore back
|
|
restoreGCState(currentAthleteTab->context);
|
|
|
|
setWindowTitle(currentAthleteTab->context->athlete->home->root().dirName());
|
|
|
|
|
|
setUpdatesEnabled(true);
|
|
}
|
|
|
|
|
|
/*----------------------------------------------------------------------
|
|
* MetricDB
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::exportMetrics()
|
|
{
|
|
// if the refresh process is running, try again when its completed
|
|
if (currentAthleteTab->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
|
|
currentAthleteTab->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(currentAthleteTab->context, fileNamesCopy);
|
|
}
|
|
}
|
|
/*----------------------------------------------------------------------
|
|
* TrainerDay
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::downloadTrainerDay()
|
|
{
|
|
QString workoutDir = appsettings->value(this, GC_WORKOUTDIR).toString();
|
|
|
|
QFileInfo fi(workoutDir);
|
|
|
|
if (fi.exists() && fi.isDir()) {
|
|
TrainerDayDownloadDialog *d = new TrainerDayDownloadDialog(currentAthleteTab->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."));
|
|
}
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Strava Routes as Workouts
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::downloadStravaRoutes()
|
|
{
|
|
QString workoutDir = appsettings->value(this, GC_WORKOUTDIR).toString();
|
|
|
|
QFileInfo fi(workoutDir);
|
|
|
|
if (fi.exists() && fi.isDir()) {
|
|
StravaRoutesDownload *d = new StravaRoutesDownload(currentAthleteTab->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(currentAthleteTab->context);
|
|
search->exec();
|
|
}
|
|
|
|
/*----------------------------------------------------------------------------
|
|
* Working with Cloud Services
|
|
* --------------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::addAccount()
|
|
{
|
|
// lets get a new cloud service account
|
|
AddCloudWizard *p = new AddCloudWizard(currentAthleteTab->context);
|
|
p->show();
|
|
}
|
|
|
|
void
|
|
MainWindow::checkCloud()
|
|
{
|
|
// kick off a check
|
|
currentAthleteTab->context->athlete->cloudAutoDownload->checkDownload();
|
|
|
|
// and auto import too whilst we're at it
|
|
currentAthleteTab->context->athlete->importFilesWhenOpeningAthlete();
|
|
}
|
|
|
|
void
|
|
MainWindow::importCloud()
|
|
{
|
|
// lets get a new cloud service account
|
|
AddCloudWizard *p = new AddCloudWizard(currentAthleteTab->context, "", true);
|
|
p->show();
|
|
}
|
|
|
|
void
|
|
MainWindow::uploadCloud(QAction *action)
|
|
{
|
|
// upload current ride, if we have one
|
|
if (currentAthleteTab->context->ride) {
|
|
// & removed to avoid issues with kde AutoCheckAccelerators
|
|
QString actionText = QString(action->text()).replace("&", "");
|
|
|
|
CloudService *db = CloudServiceFactory::instance().newService(action->data().toString(), currentAthleteTab->context);
|
|
CloudService::upload(this, currentAthleteTab->context, db, currentAthleteTab->context->ride);
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::syncCloud(QAction *action)
|
|
{
|
|
// sync with cloud
|
|
CloudService *db = CloudServiceFactory::instance().newService(action->data().toString(), currentAthleteTab->context);
|
|
CloudServiceSyncDialog sync(currentAthleteTab->context, db);
|
|
sync.exec();
|
|
}
|
|
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Utility
|
|
*--------------------------------------------------------------------*/
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Notifiers - application level events
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::configChanged(qint32)
|
|
{
|
|
// Windows and Linux menu bar should match chrome
|
|
QColor textCol(Qt::black);
|
|
if (GCColor::luminance(GColor(CTOOLBAR)) < 127) textCol = QColor(Qt::white);
|
|
QString menuColorString = GColor(CTOOLBAR).name();
|
|
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(CTOOLBAR).name()).arg(GCColor::invertColor(GColor(CTOOLBAR)).name()));
|
|
|
|
// perspective selector mimics sidebar colors
|
|
QColor selected;
|
|
if (GCColor::invertColor(GColor(CTOOLBAR)) == 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(CTOOLBAR).name()).arg(GCColor::invertColor(GColor(CTOOLBAR)).name()).arg(selected.name()));
|
|
|
|
QString buttonstyle = QString("QPushButton { border: none; border-radius: %2px; background-color: %1; "
|
|
"padding-left: 0px; padding-right: 0px; "
|
|
"padding-top: 0px; padding-bottom: 0px; }"
|
|
"QPushButton:hover { background-color: %3; }"
|
|
"QPushButton:hover:pressed { background-color: %3; }"
|
|
).arg(GColor(CTOOLBAR).name()).arg(3 * dpiXFactor).arg(GColor(CHOVER).name());
|
|
|
|
back->setStyleSheet(buttonstyle);
|
|
forward->setStyleSheet(buttonstyle);
|
|
sidelist->setStyleSheet(buttonstyle);
|
|
tabtile->setStyleSheet(buttonstyle);
|
|
lowbar->setStyleSheet(buttonstyle);
|
|
whatsthis->setStyleSheet(buttonstyle);
|
|
|
|
// All platforms
|
|
tabbar->setAutoFillBackground(true);
|
|
tabbar->setShape(QTabBar::RoundedSouth);
|
|
tabbar->setDrawBase(false);
|
|
|
|
// on select
|
|
QColor bg_select = GCColor::selectedColor(GColor(CTOOLBAR));
|
|
QColor fg_select = GCColor::invertColor(bg_select);
|
|
|
|
tabbar->setStyleSheet(QString("QTabBar::tab { background-color: %1; color: %2;}"
|
|
"QTabBar::tab::selected { background-color: %3; color: %4; }").arg(GColor(CTOOLBAR).name())
|
|
.arg(GCColor::invertColor(GColor(CTOOLBAR)).name())
|
|
.arg(bg_select.name())
|
|
.arg(fg_select.name()));
|
|
tabbar->setDocumentMode(true);
|
|
athleteView->setPalette(tabbar->palette());
|
|
|
|
head->updateGeometry();
|
|
repaint();
|
|
|
|
}
|
|
|
|
/*----------------------------------------------------------------------
|
|
* Measures
|
|
*--------------------------------------------------------------------*/
|
|
|
|
void
|
|
MainWindow::setMeasuresMenu()
|
|
{
|
|
measuresMenu->clear();
|
|
if (currentAthleteTab->context->athlete == nullptr) return;
|
|
Measures *measures = currentAthleteTab->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 (currentAthleteTab->context->athlete == nullptr) return;
|
|
Measures *measures = currentAthleteTab->context->athlete->measures;
|
|
int group = action->data().toInt();
|
|
MeasuresDownload dialog(currentAthleteTab->context, measures->getGroup(group));
|
|
dialog.exec();
|
|
}
|
|
|
|
void
|
|
MainWindow::actionClicked(int index)
|
|
{
|
|
switch(index) {
|
|
|
|
default:
|
|
case 0: currentAthleteTab->addIntervals();
|
|
break;
|
|
|
|
case 1 : splitRide();
|
|
break;
|
|
|
|
case 2 : deleteRide();
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
MainWindow::loadProgress
|
|
(QString folder, double progress)
|
|
{
|
|
Q_UNUSED(folder)
|
|
if (splash) {
|
|
splash->showMessage(QString(tr("Loading activities: %1\%")).arg(static_cast<int>(progress)));
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
MainWindow::addIntervals()
|
|
{
|
|
currentAthleteTab->addIntervals();
|
|
}
|
|
|
|
void
|
|
MainWindow::ridesAutoImport() {
|
|
|
|
currentAthleteTab->context->athlete->importFilesWhenOpeningAthlete();
|
|
|
|
}
|
|
|
|
void MainWindow::onEditMenuAboutToShow()
|
|
{
|
|
editMenu->clear();
|
|
if (toolMapper != nullptr) {
|
|
delete toolMapper;
|
|
}
|
|
|
|
// Add all the data processors to the tools menu
|
|
const DataProcessorFactory &factory = DataProcessorFactory::instance();
|
|
QList<DataProcessor*> processors = factory.getProcessorsSorted();
|
|
toolMapper = new QSignalMapper(this); // maps each option
|
|
connect(toolMapper, &QSignalMapper::mappedString, this, &MainWindow::manualProcess);
|
|
|
|
for (QList<DataProcessor*>::iterator iter = processors.begin(); iter != processors.end(); ++iter) {
|
|
if (! (*iter)->isAutomatedOnly()) {
|
|
// The localized processor name is shown in menu
|
|
QAction *action = new QAction(QString("%1...").arg((*iter)->name()), this);
|
|
editMenu->addAction(action);
|
|
connect(action, SIGNAL(triggered()), toolMapper, SLOT(map()));
|
|
toolMapper->setMapping(action, (*iter)->id());
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef GC_HAS_CLOUD_DB
|
|
void
|
|
MainWindow::cloudDBuserEditChart()
|
|
{
|
|
if (!(appsettings->cvalue(currentAthleteTab->context->athlete->cyclist, GC_CLOUDDB_TC_ACCEPTANCE, false).toBool())) {
|
|
CloudDBAcceptConditionsDialog acceptDialog(currentAthleteTab->context->athlete->cyclist);
|
|
acceptDialog.setModal(true);
|
|
if (acceptDialog.exec() == QDialog::Rejected) {
|
|
return;
|
|
};
|
|
}
|
|
|
|
if (currentAthleteTab->context->cdbChartListDialog == NULL) {
|
|
currentAthleteTab->context->cdbChartListDialog = new CloudDBChartListDialog();
|
|
}
|
|
|
|
// force refresh in prepare to allways get the latest data here
|
|
if (currentAthleteTab->context->cdbChartListDialog->prepareData(currentAthleteTab->context->athlete->cyclist, CloudDBCommon::UserEdit)) {
|
|
currentAthleteTab->context->cdbChartListDialog->exec(); // no action when closed
|
|
}
|
|
}
|
|
|
|
void
|
|
MainWindow::cloudDBuserEditUserMetric()
|
|
{
|
|
if (!(appsettings->cvalue(currentAthleteTab->context->athlete->cyclist, GC_CLOUDDB_TC_ACCEPTANCE, false).toBool())) {
|
|
CloudDBAcceptConditionsDialog acceptDialog(currentAthleteTab->context->athlete->cyclist);
|
|
acceptDialog.setModal(true);
|
|
if (acceptDialog.exec() == QDialog::Rejected) {
|
|
return;
|
|
};
|
|
}
|
|
|
|
if (currentAthleteTab->context->cdbUserMetricListDialog == NULL) {
|
|
currentAthleteTab->context->cdbUserMetricListDialog = new CloudDBUserMetricListDialog();
|
|
}
|
|
|
|
// force refresh in prepare to allways get the latest data here
|
|
if (currentAthleteTab->context->cdbUserMetricListDialog->prepareData(currentAthleteTab->context->athlete->cyclist, CloudDBCommon::UserEdit)) {
|
|
currentAthleteTab->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(currentAthleteTab->context->athlete->cyclist, GC_ATHLETE_ID, "" ).toString())) {
|
|
|
|
if (currentAthleteTab->context->cdbChartListDialog == NULL) {
|
|
currentAthleteTab->context->cdbChartListDialog = new CloudDBChartListDialog();
|
|
}
|
|
|
|
// force refresh in prepare to allways get the latest data here
|
|
if (currentAthleteTab->context->cdbChartListDialog->prepareData(currentAthleteTab->context->athlete->cyclist, CloudDBCommon::CuratorEdit)) {
|
|
currentAthleteTab->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(currentAthleteTab->context->athlete->cyclist, GC_ATHLETE_ID, "" ).toString())) {
|
|
|
|
if (currentAthleteTab->context->cdbUserMetricListDialog == NULL) {
|
|
currentAthleteTab->context->cdbUserMetricListDialog = new CloudDBUserMetricListDialog();
|
|
}
|
|
|
|
// force refresh in prepare to allways get the latest data here
|
|
if (currentAthleteTab->context->cdbUserMetricListDialog->prepareData(currentAthleteTab->context->athlete->cyclist, CloudDBCommon::CuratorEdit)) {
|
|
currentAthleteTab->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(currentAthleteTab->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);
|
|
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(currentAthleteTab->context->athlete->cyclist, s->activeSettingName(), "false").toString() != "true") continue;
|
|
|
|
if (s->capabilities() & CloudService::Query) {
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|