Athlete View 2 of 3

.. Show days since last activity and disable config icon since there
   isn't an appropriate action for now

.. Athletes can be opened and closed via the tiles and a reusable
   'Button' overview widget is available for re-use elsewhere.

.. Part 3 will enable checking for downloadable data to show an
   indicator on the tile for e.g. coaches with multiple athletes.

NOTE:

   There are a few issues regarding application context separation
   from athlete context that need fixing up (if you close the first
   athlete loaded expect crashes). Will look at this as a separate
   update since its been there for some time and is not related to
   the new view per se.
This commit is contained in:
Mark Liversedge
2020-08-03 11:30:10 +01:00
parent 7d9337fb23
commit edec952b86
10 changed files with 311 additions and 54 deletions

View File

@@ -3735,3 +3735,81 @@ ProgressBar::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*)
painter->fillRect(bar, QBrush(GColor(CPLOTMARKER)));
}
Button::Button(QGraphicsItem*parent, QString text) : QGraphicsItem(parent), text(text), state(None)
{
// not much really
setZValue(11);
setAcceptHoverEvents(true);
}
void
Button::setGeometry(double x, double y, double width, double height)
{
geom = QRectF(x,y,width, height);
}
void
Button::paint(QPainter*painter, const QStyleOptionGraphicsItem *, QWidget*)
{
// button background
QColor pc = GCColor::invertColor(GColor(CCARDBACKGROUND));
pc.setAlpha(128);
painter->setPen(QPen(pc));
QPointF pos=mapToParent(geom.x(), geom.y());
if (isUnderMouse()) {
QColor hover=GColor(CPLOTMARKER);
if (state==Clicked) hover.setAlpha(200);
else hover.setAlpha(100);
painter->setBrush(QBrush(hover));
} else painter->setBrush(QBrush(GColor(CCARDBACKGROUND)));
painter->drawRoundedRect(pos.x(), pos.y(), geom.width(), geom.height(), 20, 20);
// text using large font clipped
if (isUnderMouse()) {
QColor tc = GCColor::invertColor(CPLOTMARKER);
tc.setAlpha(200);
painter->setPen(tc);
} else {
QColor tc = GCColor::invertColor(GColor(CCARDBACKGROUND));
tc.setAlpha(200);
painter->setPen(tc);
}
painter->setFont(font);
painter->drawText(geom, text, Qt::AlignHCenter | Qt::AlignVCenter);
}
bool
Button::sceneEvent(QEvent *event)
{
if (event->type() == QEvent::GraphicsSceneHoverMove) {
// mouse moved so hover paint anyway
update();
} else if (event->type() == QEvent::GraphicsSceneHoverLeave) {
update();
} else if (event->type() == QEvent::GraphicsSceneMouseRelease) {
if (isUnderMouse() && state == Clicked) {
emit clicked();
}
state = None;
update();
} else if (event->type() == QEvent::GraphicsSceneMousePress) {
if (isUnderMouse()) state = Clicked;
update();
} else if (event->type() == QEvent::GraphicsSceneHoverEnter) {
update();
}
return false;
}

View File

@@ -654,4 +654,45 @@ class ProgressBar : public QObject, public QGraphicsItem
QPropertyAnimation *animator;
};
// simple button to use on graphics views
class Button : public QObject, public QGraphicsItem
{
Q_OBJECT
Q_INTERFACES(QGraphicsItem)
public:
Button(QGraphicsItem *parent, QString text);
void setText(QString text) { this->text = text; update(); }
void setFont(QFont font) { this->font = font; }
// we monkey around with this *A LOT*
void setGeometry(double x, double y, double width, double height);
QRectF geometry() { return geom; }
// needed as pure virtual in QGraphicsItem
void paint(QPainter*, const QStyleOptionGraphicsItem *, QWidget*);
QRectF boundingRect() const {
QPointF pos=mapToParent(geom.x(), geom.y());
return QRectF(pos.x(), pos.y(), geom.width(), geom.height());
}
// for interaction
bool sceneEvent(QEvent *event);
signals:
void clicked();
private:
QGraphicsItem *parent;
QString text;
QFont font;
QRectF geom;
enum { None, Clicked } state;
};
#endif // _GC_OverviewItem_h

View File

@@ -137,8 +137,9 @@ class Context : public QObject
// athlete load/close
void notifyLoadProgress(QString folder, double progress) { emit loadProgress(folder,progress); }
void notifyLoadCompleted(QString folder, Context *context) { emit loadCompleted(folder,context); }
void notifyLoadCompleted(QString folder, Context *context) { emit loadCompleted(folder,context); } // Athlete loaded
void notifyAthleteClose(QString folder, Context *context) { emit athleteClose(folder,context); }
void notifyLoadDone(QString folder, Context *context) { emit loadDone(folder, context); } // MainWindow finished
// preset charts
void notifyPresetsChanged() { emit presetsChanged(); }
@@ -227,6 +228,7 @@ class Context : public QObject
// loading an athlete
void loadProgress(QString,double);
void loadCompleted(QString, Context*);
void loadDone(QString, Context*);
void athleteClose(QString, Context*);
// global filter changed

View File

@@ -611,6 +611,7 @@ struct RideFileReader {
};
class MetricAggregator;
class AthleteCard;
class RideFileFactory {
private:
@@ -625,6 +626,7 @@ class RideFileFactory {
friend class ::MetricAggregator;
friend class ::RideCache;
friend class ::AthleteCard;
// will become private as code should work with
// in memory representation not on disk .. but as we

View File

@@ -1,5 +1,6 @@
#include "AthleteView.h"
#include "TabView.h"
#include "JsonRideFile.h" // for DATETIME_FORMAT
// athlete card
static bool _registerItems()
@@ -15,7 +16,9 @@ static bool registered = _registerItems();
static const int gl_athletes_per_row = 5;
static const int gl_athletes_deep = 15;
static const int gl_avatar_width = 5;
static const int gl_progress_width = ROWHEIGHT / 2;
static const int gl_progress_width = ROWHEIGHT/2;
static const int gl_button_height = ROWHEIGHT*1.5;
static const int gl_button_width = ROWHEIGHT*5;
AthleteView::AthleteView(Context *context) : ChartSpace(context, OverviewScope::ATHLETES)
{
@@ -31,6 +34,12 @@ AthleteView::AthleteView(Context *context) : ChartSpace(context, OverviewScope::
QString name = i.next();
// if there is no avatar its not a real folder, even a blank athlete
// will get the default avatar. This is to avoid having to work out
// what all the random folders are that Qt creates on Windows when
// using QtWebEngine, or Python or R or created by user scripts
if (!QFile(gcroot + "/" + name + "/config/avatar.png").exists()) continue;
// add a card for each athlete
AthleteCard *ath = new AthleteCard(this, name);
addItem(row,col,gl_athletes_deep,ath);
@@ -59,17 +68,8 @@ AthleteView::configChanged(qint32)
AthleteCard::AthleteCard(ChartSpace *parent, QString path) : ChartSpaceItem(parent, path), path(path)
{
// context and signals
if (parent->context->athlete->cyclist == path) {
context = parent->context;
loadprogress = 100;
} else {
context = NULL;
loadprogress = 0;
}
connect(parent->context->mainWindow, SIGNAL(openingAthlete(QString,Context*)), this, SLOT(opening(QString,Context*)));
connect(parent->context, SIGNAL(athleteClose(QString,Context*)), this, SLOT(closing(QString,Context*)));
// no config icon thanks
setShowConfig(false);
// avatar
QRectF img(ROWHEIGHT,ROWHEIGHT*2,ROWHEIGHT* gl_avatar_width, ROWHEIGHT* gl_avatar_width);
@@ -84,22 +84,76 @@ AthleteCard::AthleteCard(ChartSpace *parent, QString path) : ChartSpaceItem(pare
painter.drawEllipse(0, 0, img.width(), img.height());
avatar = canvas.toImage();
#if 0
// ridecache raw
if (loadprogress == 0) {
QTime timer;
timer.start();
QFile rideDB(gcroot + "/" + path + "/cache/rideDB.json");
if (rideDB.exists() && rideDB.open(QFile::ReadOnly)) {
// open close button
button = new Button(this, tr("Open"));
button->setFont(parent->midfont);
button->setGeometry(geometry().width()-(gl_button_width+ROWHEIGHT), geometry().height()-(gl_button_height+ROWHEIGHT),
gl_button_width, gl_button_height);
connect(button, SIGNAL(clicked()), this, SLOT(clicked()));
QByteArray contents = rideDB.readAll();
rideDB.close();
QJsonDocument json = QJsonDocument::fromJson(contents);
}
fprintf(stderr, "'%s' read rideDB took %d usecs\n", path.toStdString().c_str(), timer.elapsed()); fflush(stderr);
// context and signals
if (parent->context->athlete->cyclist == path) {
context = parent->context;
loadprogress = 100;
button->setText("Close");
} else {
context = NULL;
loadprogress = 0;
}
#endif
connect(parent->context->mainWindow, SIGNAL(openingAthlete(QString,Context*)), this, SLOT(opening(QString,Context*)));
connect(parent->context, SIGNAL(athleteClose(QString,Context*)), this, SLOT(closing(QString,Context*)));
// set stats to none
count=0;
last=QDateTime();
// need to fetch by looking at file names, rideDB parse is too expensive here
if (loadprogress == 0) {
// unloaded athletes we just say when last activity was
QDate today = QDateTime::currentDateTime().date();
QDir dir(gcroot + "/" + path + "/activities");
QStringListIterator i(RideFileFactory::instance().listRideFiles(dir));
while (i.hasNext()) {
QString name = i.next();
QDateTime date;
if (RideFile::parseRideFileName(name, &date)) {
//int ago= date.date().daysTo(today);
if (last==QDateTime() || date > last) last = date;
count++;
}
}
} else {
// use ridecache
refreshStats();
}
}
void
AthleteCard::refreshStats()
{
if (context == NULL) return;
// last 90 days
count=context->athlete->rideCache->rides().count();
foreach(RideItem *item, context->athlete->rideCache->rides()) {
// last activity date?
if (last==QDateTime() || item->dateTime > last) last = item->dateTime;
}
}
void
AthleteCard::clicked()
{
if (loadprogress==100) parent->context->mainWindow->closeTab(path);
else parent->context->mainWindow->openTab(path);
}
void
@@ -109,18 +163,44 @@ AthleteCard::opening(QString name, Context*context)
if (name == path) {
this->context = context;
loadprogress = 100;
button->setText("Close");
button->hide();
connect(context,SIGNAL(loadProgress(QString,double)), this, SLOT(loadProgress(QString,double)));
connect(context,SIGNAL(loadDone(QString,Context*)), this, SLOT(loadDone(QString,Context*)));
connect(context,SIGNAL(athleteClose(QString,Context*)), this, SLOT(closing(QString,Context*)));
}
}
void AthleteCard::itemGeometryChanged()
{
button->setGeometry(geometry().width()-(gl_button_width+ROWHEIGHT), geometry().height()-(gl_button_height+ROWHEIGHT),
gl_button_width, gl_button_height);
}
void AthleteCard::loadDone(QString name, Context *)
{
if (this->name == name) {
loadprogress = 100;
refreshStats();
button->show();
update();
}
}
void AthleteCard::dragChanged(bool drag)
{
if (drag || (loadprogress != 100 && loadprogress != 0)) button->hide();
else button->show();
}
void
AthleteCard::closing(QString name, Context *context)
AthleteCard::closing(QString name, Context *)
{
if (name == path) {
this->context = NULL;
loadprogress = 0;
button->setText("Open");
update();
}
}
@@ -139,6 +219,18 @@ AthleteCard::itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWid
QRectF img(ROWHEIGHT,ROWHEIGHT*2,ROWHEIGHT* gl_avatar_width, ROWHEIGHT* gl_avatar_width);
painter->drawImage(img, avatar);
// last workout if nothing recent
if (/*maxy == 0 && */last != QDateTime() || count == 0) {
QRectF rectf = QRectF(ROWHEIGHT,geometry().height()-(ROWHEIGHT*5), geometry().width()-(ROWHEIGHT*2), ROWHEIGHT*1.5);
QString message;
if (count == 0) message = "No activities.";
else message = QString("Last workout %1 days ago").arg(last.daysTo(QDateTime::currentDateTime()));
painter->setFont(parent->midfont);
painter->setPen(QColor(150,150,150));
painter->drawText(rectf, message, Qt::AlignHCenter | Qt::AlignVCenter);
}
// load status
QRectF progressbar(0, geometry().height()-gl_progress_width, geometry().width() * (loadprogress/100), gl_progress_width);
painter->fillRect(progressbar, QBrush(GColor(CPLOTMARKER)));

View File

@@ -23,11 +23,15 @@ class AthleteCard : public ChartSpaceItem
AthleteCard(ChartSpace *parent, QString path);
void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);
void itemGeometryChanged() {}
void dragChanged(bool);
void itemGeometryChanged();
void setData(RideItem *) {}
void setDateRange(DateRange) {}
QWidget *config() { return new OverviewItemConfig(this); }
// refresh stats on last workouts etc
void refreshStats();
// create and config
static ChartSpaceItem *create(ChartSpace *parent) { return new AthleteCard(parent, ""); }
@@ -35,11 +39,19 @@ class AthleteCard : public ChartSpaceItem
void opening(QString, Context*);
void closing(QString, Context*);
void loadProgress(QString, double);
void loadDone(QString, Context*);
void clicked();
private:
double loadprogress;
Context *context;
QString path;
QImage avatar;
Button *button;
// little graph of last 90 days
int count; // total activities
QDateTime last; // date of last activity recorded
};

View File

@@ -236,6 +236,7 @@ ChartSpaceItem::sceneEvent(QEvent *event)
bool
ChartSpaceItem::inHotspot()
{
if (showconfig == false) return false;
QPoint vpos = parent->view->mapFromGlobal(QCursor::pos());
QPointF spos = parent->view->mapToScene(vpos);
@@ -248,6 +249,8 @@ ChartSpaceItem::inHotspot()
bool
ChartSpaceItem::inCorner()
{
if (showconfig == false) return false;
QPoint vpos = parent->view->mapFromGlobal(QCursor::pos());
QPointF spos = parent->view->mapToScene(vpos);
@@ -296,33 +299,37 @@ ChartSpaceItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opt, QW
if (drag) return;
// not dragging so we can get to work painting the rest
if (parent->state != ChartSpace::DRAG && underMouse()) {
if (inCorner()) {
// config icon
if (showconfig) {
if (parent->state != ChartSpace::DRAG && underMouse()) {
// if hovering over the button show a background to indicate
// that pressing a button is good
QPainterPath path;
path.addRoundedRect(QRectF(geometry().width()-40-ROWHEIGHT,0,
ROWHEIGHT+40, ROWHEIGHT+40), ROWHEIGHT/5, ROWHEIGHT/5);
painter->setPen(Qt::NoPen);
QColor darkgray(GColor(CCARDBACKGROUND).lighter(200));
painter->setBrush(darkgray);
painter->drawPath(path);
painter->fillRect(QRectF(geometry().width()-40-ROWHEIGHT, 0, ROWHEIGHT+40-(ROWHEIGHT/5), ROWHEIGHT+40), QBrush(darkgray));
painter->fillRect(QRectF(geometry().width()-40-ROWHEIGHT, ROWHEIGHT/5, ROWHEIGHT+40, ROWHEIGHT+40-(ROWHEIGHT/5)), QBrush(darkgray));
if (inCorner()) {
// draw the config button and make it more obvious
// when hovering over the card
painter->drawPixmap(geometry().width()-20-(ROWHEIGHT*1), 20, ROWHEIGHT*1, ROWHEIGHT*1, accentConfig.pixmap(QSize(ROWHEIGHT*1, ROWHEIGHT*1)));
// if hovering over the button show a background to indicate
// that pressing a button is good
QPainterPath path;
path.addRoundedRect(QRectF(geometry().width()-40-ROWHEIGHT,0,
ROWHEIGHT+40, ROWHEIGHT+40), ROWHEIGHT/5, ROWHEIGHT/5);
painter->setPen(Qt::NoPen);
QColor darkgray(GColor(CCARDBACKGROUND).lighter(200));
painter->setBrush(darkgray);
painter->drawPath(path);
painter->fillRect(QRectF(geometry().width()-40-ROWHEIGHT, 0, ROWHEIGHT+40-(ROWHEIGHT/5), ROWHEIGHT+40), QBrush(darkgray));
painter->fillRect(QRectF(geometry().width()-40-ROWHEIGHT, ROWHEIGHT/5, ROWHEIGHT+40, ROWHEIGHT+40-(ROWHEIGHT/5)), QBrush(darkgray));
} else {
// draw the config button and make it more obvious
// when hovering over the card
painter->drawPixmap(geometry().width()-20-(ROWHEIGHT*1), 20, ROWHEIGHT*1, ROWHEIGHT*1, accentConfig.pixmap(QSize(ROWHEIGHT*1, ROWHEIGHT*1)));
// hover on card - make it more obvious there is a config button
painter->drawPixmap(geometry().width()-20-(ROWHEIGHT*1), 20, ROWHEIGHT*1, ROWHEIGHT*1, whiteConfig.pixmap(QSize(ROWHEIGHT*1, ROWHEIGHT*1)));
}
} else {
} else painter->drawPixmap(geometry().width()-20-(ROWHEIGHT*1), 20, ROWHEIGHT*1, ROWHEIGHT*1, grayConfig.pixmap(QSize(ROWHEIGHT*1, ROWHEIGHT*1)));
// hover on card - make it more obvious there is a config button
painter->drawPixmap(geometry().width()-20-(ROWHEIGHT*1), 20, ROWHEIGHT*1, ROWHEIGHT*1, whiteConfig.pixmap(QSize(ROWHEIGHT*1, ROWHEIGHT*1)));
}
} else painter->drawPixmap(geometry().width()-20-(ROWHEIGHT*1), 20, ROWHEIGHT*1, ROWHEIGHT*1, grayConfig.pixmap(QSize(ROWHEIGHT*1, ROWHEIGHT*1)));
}
// thin border
if (!drag) {
@@ -337,7 +344,10 @@ ChartSpaceItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opt, QW
painter->drawPath(path);
}
// item paints itself and can completely repaint everything
// including the title etc, in case this is useful
itemPaint(painter, opt, widget);
}
static bool ChartSpaceItemSort(const ChartSpaceItem* left, const ChartSpaceItem* right)

View File

@@ -66,13 +66,18 @@ class ChartSpaceItem : public QGraphicsWidget
virtual QWidget *config()=0; // must supply a widget to configure
// turn off/on the config corner button
void setShowConfig(bool x) { showconfig=x; update(); }
bool showConfig() const { return showconfig; }
// what type am I- managed by user
int type;
ChartSpaceItem(ChartSpace *parent, QString name) : QGraphicsWidget(NULL),
parent(parent), name(name),
column(0), order(0), deep(5), onscene(false),
placing(false), drag(false), invisible(false) {
placing(false), drag(false), invisible(false),
showconfig(true) {
setAutoFillBackground(false);
setFlags(flags() | QGraphicsItem::ItemClipsToShape); // don't paint outside the card
@@ -119,6 +124,7 @@ class ChartSpaceItem : public QGraphicsWidget
bool onscene, placing, drag;
bool incorner;
bool invisible;
bool showconfig;
// base paint
void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);

View File

@@ -1727,6 +1727,9 @@ MainWindow::loadCompleted(QString name, Context *context)
// 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();
}
@@ -1751,6 +1754,18 @@ MainWindow::closeTabClicked(int index)
removeTab(tab);
}
bool
MainWindow::closeTab(QString name)
{
for(int i=0; i<tabbar->count(); i++) {
if (name == tabbar->tabText(i)) {
closeTabClicked(i);
return true;
}
}
return false;
}
bool
MainWindow::closeTab()
{
@@ -2157,10 +2172,8 @@ MainWindow::configChanged(qint32)
tabbar->setShape(QTabBar::RoundedSouth);
tabbar->setDrawBase(false);
if (GCColor::isFlat())
tabbarPalette.setBrush(backgroundRole(), GColor(CCHROME));
else
tabbarPalette.setBrush(backgroundRole(), QColor("#B3B4B6"));
tabbarPalette.setBrush(backgroundRole(), GColor(CCHROME));
tabbarPalette.setBrush(foregroundRole(), GCColor::invertColor(GColor(CCHROME)));
tabbar->setPalette(tabbarPalette);
athleteView->setPalette(tabbarPalette);

View File

@@ -150,6 +150,7 @@ class MainWindow : public QMainWindow
void openTab(QString name);
void loadCompleted(QString name, Context *context);
void closeTabClicked(int index); // user clicked to close tab
bool closeTab(QString name); // close named athlete
bool closeTab(); // close current, might not if the user
// changes mind if there are unsaved changes.
void removeTab(Tab*); // remove without question