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