Refactor OverviewWindow for ChartSpace

.. the OverviewWindow class has been refactored to extract out the
   core dashboard UX/UI into a new class ChartSpace.

.. additionally, a ChartSpace contains a set of ChartSpaceItem which
   need to be subclassed by the developer.

.. for the Overview a set of OverwiewItem classes have been introduced
   such as RPEOverviewItem, MetricOverviewItem and so on. These are
   subclasses of the ChartSpaceItem.

.. The overview window implementation is now mostly configuration and
   assembly of a dashboard using OverviewItems and the ChartSpace.

.. This refactor is to enable the ChartSpace to be used as a drop in
   replacement for the existing TabView.

.. There are no functional enhancements in this commit, but the
   overview chart shouls appear to be unchanged by the user.
This commit is contained in:
Mark Liversedge
2020-05-31 18:16:13 +01:00
parent 2d3eed0a98
commit 8a1842a9ba
10 changed files with 4009 additions and 9 deletions

View File

@@ -65,12 +65,6 @@ OverviewWindow::OverviewWindow(Context *context) :
view->setRenderHint(QPainter::Antialiasing, true);
view->setFrameStyle(QFrame::NoFrame);
view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
#ifdef Q_OS_LINUX
if (QGLFormat::openGLVersionFlags().testFlag(QGLFormat::OpenGL_Version_2_0)) {
view->setViewport(new QGLWidget(QGLFormat(QGL::SampleBuffers)));
view->setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
}
#endif
view->setScene(scene);
// layout

325
src/Charts/Overview.cpp Normal file
View File

@@ -0,0 +1,325 @@
/*
* Copyright (c) 2017 Mark Liversedge (liversedge@gmail.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "Overview.h"
#include "ChartSpace.h"
#include "OverviewItems.h"
static QIcon grayConfig, whiteConfig, accentConfig;
OverviewWindow::OverviewWindow(Context *context) : GcChartWindow(context), context(context), configured(false)
{
setContentsMargins(0,0,0,0);
setProperty("color", GColor(COVERVIEWBACKGROUND));
//setProperty("nomenu", true);
setShowTitle(false);
setControls(NULL);
QHBoxLayout *main = new QHBoxLayout;
space = new ChartSpace(context);
main->addWidget(space);
// all the widgets
setChartLayout(main);
// tell space when a ride is selected
connect(this, SIGNAL(rideItemChanged(RideItem*)), space, SLOT(rideSelected(RideItem*)));
}
QString
OverviewWindow::getConfiguration() const
{
// return a JSON snippet to represent the entire config
QString config;
// setup
config = "{\n \"version\":\"2.0\",\n";
config += " \"CHARTS\":[\n";
// do cards
foreach(ChartSpaceItem *item, space->allItems()) {
// basic stuff first - name, type etc
config += " { ";
config += "\"type\":" + QString("%1").arg(static_cast<int>(item->type)) + ",";
config += "\"deep\":" + QString("%1").arg(item->deep) + ",";
config += "\"column\":" + QString("%1").arg(item->column) + ",";
config += "\"order\":" + QString("%1").arg(item->order) + ",";
// now the actual card settings
switch(item->type) {
case OverviewItemType::RPE:
{
RPEOverviewItem *rpe = reinterpret_cast<RPEOverviewItem*>(item);
}
break;
case OverviewItemType::METRIC:
{
MetricOverviewItem *metric = reinterpret_cast<MetricOverviewItem*>(item);
config += "\"symbol\":\"" + QString("%1").arg(metric->symbol) + "\",";
}
break;
case OverviewItemType::META:
{
MetaOverviewItem *meta = reinterpret_cast<MetaOverviewItem*>(item);
config += "\"symbol\":\"" + QString("%1").arg(meta->symbol) + "\",";
}
break;
case OverviewItemType::PMC:
{
PMCOverviewItem *pmc = reinterpret_cast<PMCOverviewItem*>(item);
config += "\"symbol\":\"" + QString("%1").arg(pmc->symbol) + "\",";
}
break;
case OverviewItemType::ROUTE:
{
RouteOverviewItem *route = reinterpret_cast<RouteOverviewItem*>(item);
}
break;
case OverviewItemType::INTERVAL:
{
IntervalOverviewItem *interval = reinterpret_cast<IntervalOverviewItem*>(item);
config += "\"xsymbol\":\"" + QString("%1").arg(interval->xsymbol) + "\",";
config += "\"ysymbol\":\"" + QString("%1").arg(interval->ysymbol) + "\",";
config += "\"zsymbol\":\"" + QString("%1").arg(interval->zsymbol) + "\"" + ",";
}
break;
case OverviewItemType::ZONE:
{
ZoneOverviewItem *zone = reinterpret_cast<ZoneOverviewItem*>(item);
config += "\"series\":" + QString("%1").arg(static_cast<int>(zone->series)) + ",";
}
break;
}
config += "\"name\":\"" + item->name + "\"";
config += " }";
if (space->allItems().last() != item) config += ",";
config += "\n";
}
config += " ]\n}\n";
return config;
}
void
OverviewWindow::setConfiguration(QString config)
{
// XXX hack because we're not in the default layout and don't want to
// XXX this is just to handle setup for the very first time its run !
if (configured == true) return;
configured = true;
// DEFAULT CONFIG (FOR NOW WHEN NOT IN THE DEFAULT LAYOUT)
//
// default column widths - max 10 columns;
// note the sizing is such that each card is the equivalent of a full screen
// so we can embed charts etc without compromising what they can display
defaultsetup: // I know, but its easier than lots of nested if clauses above
if (config == "") {
// column 0
ChartSpaceItem *add;
add = new PMCOverviewItem(space, "coggan_tss");
space->addItem(1,0,9, add);
add = new MetaOverviewItem(space, "Sport", "Sport");
space->addItem(2,0,5, add);
add = new MetaOverviewItem(space, "Workout Code", "Workout Code");
space->addItem(3,0,5, add);
add = new MetricOverviewItem(space, "Duration", "workout_time");
space->addItem(4,0,9, add);
add = new MetaOverviewItem(space, "Notes", "Notes");
space->addItem(5,0,13, add);
// column 1
add = new MetricOverviewItem(space, "HRV rMSSD", "rMSSD");
space->addItem(1,1,9, add);
add = new MetricOverviewItem(space, "Heartrate", "average_hr");
space->addItem(2,1,5, add);
add = new ZoneOverviewItem(space, "Heartrate Zones", RideFile::hr);
space->addItem(3,1,11, add);
add = new MetricOverviewItem(space, "Climbing", "elevation_gain");
space->addItem(4,1,5, add);
add = new MetricOverviewItem(space, "Cadence", "average_cad");
space->addItem(5,1,5, add);
add = new MetricOverviewItem(space, "Work", "total_work");
space->addItem(6,1,5, add);
// column 2
add = new RPEOverviewItem(space, "RPE");
space->addItem(1,2,9, add);
add = new MetricOverviewItem(space, "Stress", "coggan_tss");
space->addItem(2,2,5, add);
add = new ZoneOverviewItem(space, "Fatigue Zones", RideFile::wbal);
space->addItem(3,2,11, add);
add = new IntervalOverviewItem(space, "Intervals", "elapsed_time", "average_power", "workout_time");
space->addItem(4,2,17, add);
// column 3
add = new MetricOverviewItem(space, "Power", "average_power");
space->addItem(1,3,9, add);
add = new MetricOverviewItem(space, "IsoPower", "coggan_np");
space->addItem(2,3,5, add);
add = new ZoneOverviewItem(space, "Power Zones", RideFile::watts);
space->addItem(3,3,11, add);
add = new MetricOverviewItem(space, "Peak Power Index", "peak_power_index");
space->addItem(4,3,8, add);
add = new MetricOverviewItem(space, "Variability", "coggam_variability_index");
space->addItem(5,3,8, add);
// column 4
add = new MetricOverviewItem(space, "Distance", "total_distance");
space->addItem(1,4,9, add);
add = new MetricOverviewItem(space, "Speed", "average_speed");
space->addItem(2,4,5, add);
add = new ZoneOverviewItem(space, "Pace Zones", RideFile::kph);
space->addItem(3,4,11, add);
add = new RouteOverviewItem(space, "Route");
space->addItem(4,4,17, add);
} else {
//
// But by default we parse and apply (dropping back to default setup on error)
//
// parse
QJsonDocument doc = QJsonDocument::fromJson(config.toUtf8());
if (doc.isEmpty() || doc.isNull()) {
config="";
goto defaultsetup;
}
// parsed so lets work through it and setup the overview
QJsonObject root = doc.object();
// check version
QString version = root["version"].toString();
if (version != "2.0") {
config="";
goto defaultsetup;
}
// cards
QJsonArray CHARTS = root["CHARTS"].toArray();
foreach(const QJsonValue val, CHARTS) {
// convert so we can inspect
QJsonObject obj = val.toObject();
// get the basics
QString name = obj["name"].toString();
int column = obj["column"].toInt();
int order = obj["order"].toInt();
int deep = obj["deep"].toInt();
int type = obj["type"].toInt();
// lets create the cards
ChartSpaceItem *add=NULL;
switch(type) {
case OverviewItemType::RPE :
{
add = new RPEOverviewItem(space, name);
space->addItem(order,column,deep, add);
}
break;
case OverviewItemType::METRIC :
{
QString symbol=obj["symbol"].toString();
add = new MetricOverviewItem(space, name,symbol);
space->addItem(order,column,deep, add);
}
break;
case OverviewItemType::META :
{
QString symbol=obj["symbol"].toString();
add = new MetaOverviewItem(space, name,symbol);
space->addItem(order,column,deep, add);
}
break;
case OverviewItemType::PMC :
{
QString symbol=obj["symbol"].toString();
add = new PMCOverviewItem(space, symbol); // doesn't have a title
space->addItem(order,column,deep, add);
}
break;
case OverviewItemType::ZONE :
{
RideFile::SeriesType series = static_cast<RideFile::SeriesType>(obj["series"].toInt());
add = new ZoneOverviewItem(space, name, series);
space->addItem(order,column,deep, add);
}
break;
case OverviewItemType::ROUTE :
{
add = new RouteOverviewItem(space, name); // doesn't have a title
space->addItem(order,column,deep, add);
}
break;
case OverviewItemType::INTERVAL :
{
QString xsymbol=obj["xsymbol"].toString();
QString ysymbol=obj["ysymbol"].toString();
QString zsymbol=obj["zsymbol"].toString();
add = new IntervalOverviewItem(space, name, xsymbol, ysymbol, zsymbol); // doesn't have a title
space->addItem(order,column,deep, add);
}
break;
}
}
}
// put in place
space->updateGeometry();
}

61
src/Charts/Overview.h Normal file
View File

@@ -0,0 +1,61 @@
/*
* Copyright (c) 2020 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
*/
#ifndef _GC_OverviewWindow_h
#define _GC_OverviewWindow_h 1
// basics
#include "GoldenCheetah.h"
#include "Settings.h"
#include "Units.h"
#include "Colors.h"
#include "Context.h"
#include "Athlete.h"
#include "RideItem.h"
#include "RideMetric.h"
#include "HrZones.h"
#include "ChartSpace.h"
class OverviewWindow : public GcChartWindow
{
Q_OBJECT
Q_PROPERTY(QString config READ getConfiguration WRITE setConfiguration USER true)
public:
OverviewWindow(Context *context);
// used by children
Context *context;
public slots:
// get/set config
QString getConfiguration() const;
void setConfiguration(QString x);
private:
// gui setup
ChartSpace *space;
bool configured;
};
#endif // _GC_OverviewWindow_h

2054
src/Charts/OverviewItems.cpp Normal file

File diff suppressed because it is too large Load Diff

381
src/Charts/OverviewItems.h Normal file
View File

@@ -0,0 +1,381 @@
/*
* Copyright (c) 2020 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
*/
#ifndef _GC_OverviewItem_h
#define _GC_OverviewItem_h 1
// basics
#include "ChartSpace.h"
#include <QGraphicsItem>
// qt charts for zone chart
#include <QtCharts>
#include <QBarSet>
#include <QBarSeries>
#include <QLineSeries>
// subwidgets for viz inside each overview item
class RPErating;
class BPointF;
class Sparkline;
class BubbleViz;
class Routeline;
// sparklines number of points - look back 6 weeks
#define SPARKDAYS 42
// number of points in a route viz - trial and error gets 250 being reasonable
#define ROUTEPOINTS 250
// types we use
enum OverviewItemType { RPE, METRIC, META, ZONE, INTERVAL, PMC, ROUTE };
class RPEOverviewItem : public ChartSpaceItem
{
Q_OBJECT
public:
RPEOverviewItem(ChartSpace *parent, QString name);
void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);
void itemGeometryChanged();
void setData(RideItem *item);
// RPE meta field
Sparkline *sparkline;
RPErating *rperating;
// for setting sparkline & painting
bool up, showrange;
QString value, upper, lower, mean;
};
class MetricOverviewItem : public ChartSpaceItem
{
Q_OBJECT
public:
MetricOverviewItem(ChartSpace *parent, QString name, QString symbol);
void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);
void itemGeometryChanged();
void setData(RideItem *item);
QString symbol;
RideMetric *metric;
QString units;
bool up, showrange;
QString value, upper, lower, mean;
Sparkline *sparkline;
};
class MetaOverviewItem : public ChartSpaceItem
{
Q_OBJECT
public:
MetaOverviewItem(ChartSpace *parent, QString name, QString symbol);
void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);
void itemGeometryChanged();
void setData(RideItem *item);
QString symbol;
int fieldtype;
// for numeric metadata items
bool up, showrange;
QString value, upper, lower, mean;
Sparkline *sparkline;
};
class PMCOverviewItem : public ChartSpaceItem
{
Q_OBJECT
public:
PMCOverviewItem(ChartSpace *parent, QString symbol);
void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);
void itemGeometryChanged();
void setData(RideItem *item);
QString symbol;
double sts, lts, sb, rr, stress;
};
class ZoneOverviewItem : public ChartSpaceItem
{
Q_OBJECT
public:
ZoneOverviewItem(ChartSpace *parent, QString name, RideFile::seriestype);
void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);
void itemGeometryChanged();
void setData(RideItem *item);
void dragChanged(bool x);
RideFile::seriestype series;
QChart *chart;
QBarSet *barset;
QBarSeries *barseries;
QStringList categories;
QBarCategoryAxis *barcategoryaxis;
};
class RouteOverviewItem : public ChartSpaceItem
{
Q_OBJECT
public:
RouteOverviewItem(ChartSpace *parent, QString name);
void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);
void itemGeometryChanged();
void setData(RideItem *item);
Routeline *routeline;
};
class IntervalOverviewItem : public ChartSpaceItem
{
Q_OBJECT
public:
IntervalOverviewItem(ChartSpace *parent, QString name, QString xs, QString ys, QString zs);
void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);
void itemGeometryChanged();
void setData(RideItem *item);
QString xsymbol, ysymbol, zsymbol;
BubbleViz *bubble;
};
//
// below are theviz widgets used by the overview items
//
// for now the basics are x and y and a radius z, color fill
class BPointF {
public:
BPointF() : x(0), y(0), z(0), fill(GColor(Qt::gray)) {}
double score(BPointF &other);
double x,y,z;
QColor fill;
QString label;
};
// bubble chart, very very basic just a visualisation
class BubbleViz : public QObject, public QGraphicsItem
{
// need to be a qobject for metaproperties
Q_OBJECT
Q_INTERFACES(QGraphicsItem)
// want a meta property for property animation
Q_PROPERTY(int transition READ getTransition WRITE setTransition)
public:
BubbleViz(IntervalOverviewItem *parent, QString name=""); // create and say how many days
// we monkey around with this *A LOT*
void setGeometry(double x, double y, double width, double height);
QRectF geometry() { return geom; }
// transition animation 0-255
int getTransition() const {return transition;}
void setTransition(int x) { if (transition !=x) {transition=x; update();}}
// null members for now just get hooked up
void setPoints(QList<BPointF>points);
void setRange(double minx, double maxx, double miny, double maxy) {
oldminx = this->minx;
oldminy = this->miny;
oldmaxx = this->maxx;
oldmaxy = this->maxy;
this->minx=minx;
this->maxx=maxx;
this->miny=miny;
this->maxy=maxy;
}
void setAxisNames(QString xlabel, QString ylabel) { this->xlabel=xlabel; this->ylabel=ylabel; update(); }
// needed as pure virtual in QGraphicsItem
QVariant itemChange(GraphicsItemChange change, const QVariant &value);
void paint(QPainter*, const QStyleOptionGraphicsItem *, QWidget*);
QRectF boundingRect() const { return QRectF(parent->geometry().x() + geom.x(),
parent->geometry().y() + geom.y(),
geom.width(), geom.height());
}
// watch hovering
bool sceneEvent(QEvent *event);
private:
IntervalOverviewItem *parent;
QRectF geom;
QString name;
// where is the cursor?
bool hover;
QPointF plotpos;
// for animated transition
QList <BPointF> oldpoints; // for animation
int transition;
double oldmean;
double oldminx,oldmaxx,oldminy,oldmaxy;
QPropertyAnimation *animator;
// chart settings
QList <BPointF> points;
double minx,maxx,miny,maxy;
QString xlabel, ylabel;
double mean, max;
};
// RPE rating viz and widget to set value
class RPErating : public QGraphicsItem
{
public:
RPErating(RPEOverviewItem *parent, QString name=""); // create and say how many days
// we monkey around with this *A LOT*
void setGeometry(double x, double y, double width, double height);
QRectF geometry() { return geom; }
void setValue(QString);
// needed as pure virtual in QGraphicsItem
QVariant itemChange(GraphicsItemChange change, const QVariant &value);
void paint(QPainter*, const QStyleOptionGraphicsItem *, QWidget*);
QRectF boundingRect() const { return QRectF(parent->geometry().x() + geom.x(),
parent->geometry().y() + geom.y(),
geom.width(), geom.height());
}
// for interaction
bool sceneEvent(QEvent *event);
void cancelEdit();
void applyEdit();
private:
RPEOverviewItem *parent;
QString name;
QString description;
QRectF geom;
QString value, oldvalue;
QColor color;
// interaction
bool hover;
};
// tufte style sparkline to plot metric history
class Sparkline : public QGraphicsItem
{
public:
Sparkline(QGraphicsWidget *parent, int count,QString name=""); // create and say how many days
// we monkey around with this *A LOT*
void setGeometry(double x, double y, double width, double height);
QRectF geometry() { return geom; }
void setPoints(QList<QPointF>);
void setRange(double min, double max); // upper lower
// needed as pure virtual in QGraphicsItem
QVariant itemChange(GraphicsItemChange change, const QVariant &value);
void paint(QPainter*, const QStyleOptionGraphicsItem *, QWidget*);
QRectF boundingRect() const { return QRectF(parent->geometry().x() + geom.x(),
parent->geometry().y() + geom.y(),
geom.width(), geom.height());
}
private:
QGraphicsWidget *parent;
QRectF geom;
QString name;
double min, max;
QList<QPointF> points;
};
// visualisation of a GPS route as a shape
class Routeline : public QObject, public QGraphicsItem
{
Q_OBJECT
Q_INTERFACES(QGraphicsItem)
Q_PROPERTY(int transition READ getTransition WRITE setTransition)
public:
Routeline(QGraphicsWidget *parent, QString name=""); // create and say how many days
// transition animation 0-255
int getTransition() const {return transition;}
void setTransition(int x) { if (transition !=x) {transition=x; update();}}
// we monkey around with this *A LOT*
void setGeometry(double x, double y, double width, double height);
QRectF geometry() { return geom; }
void setData(RideItem *item);
// needed as pure virtual in QGraphicsItem
QVariant itemChange(GraphicsItemChange change, const QVariant &value);
void paint(QPainter*, const QStyleOptionGraphicsItem *, QWidget*);
QRectF boundingRect() const { return QRectF(parent->geometry().x() + geom.x(),
parent->geometry().y() + geom.y(),
geom.width(), geom.height());
}
private:
QGraphicsWidget *parent;
QRectF geom;
QString name;
QPainterPath path, oldpath;
double width, height; // size of painterpath, so we scale to fit on paint
// animating
int transition;
QPropertyAnimation *animator;
double owidth, oheight; // size of painterpath, so we scale to fit on paint
};
#endif // _GC_OverviewItem_h

946
src/Gui/ChartSpace.cpp Normal file
View File

@@ -0,0 +1,946 @@
/*
* Copyright (c) 2020 Mark Liversedge (liversedge@gmail.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "ChartSpace.h"
#include "TabView.h"
#include "Athlete.h"
#include "RideCache.h"
#include <cmath>
#include <QGraphicsSceneMouseEvent>
#include <QGLWidget>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
static QIcon grayConfig, whiteConfig, accentConfig;
ChartSpace::ChartSpace(Context *context) :
state(NONE), context(context), group(NULL), _viewY(0),
yresizecursor(false), xresizecursor(false), block(false), scrolling(false),
setscrollbar(false), lasty(-1)
{
setContentsMargins(0,0,0,0);
// no longer a gc chart ...
//setProperty("color", GColor(COVERVIEWBACKGROUND)); XXX??
//setProperty("nomenu", true);
//setShowTitle(false);
//setControls(NULL);
QHBoxLayout *main = new QHBoxLayout;
// add a view and scene and centre
scene = new QGraphicsScene(this);
view = new QGraphicsView(this);
view->viewport()->setAttribute(Qt::WA_AcceptTouchEvents, false); // stops it stealing focus on mouseover
scrollbar = new QScrollBar(Qt::Vertical, this);
// how to move etc
//view->setDragMode(QGraphicsView::ScrollHandDrag);
view->setRenderHint(QPainter::Antialiasing, true);
view->setFrameStyle(QFrame::NoFrame);
view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
view->setScene(scene);
// layout
main->addWidget(view);
main->addWidget(scrollbar);
// all the widgets
setLayout(main);
// by default these are the column sizes (user can adjust)
columns << 1200 << 1200 << 1200 << 1200 << 1200 << 1200 << 1200 << 1200 << 1200 << 1200;
// for changing the view
group = new QParallelAnimationGroup(this);
viewchange = new QPropertyAnimation(this, "viewRect");
viewchange->setEasingCurve(QEasingCurve(QEasingCurve::OutQuint));
// for scrolling the view
scroller = new QPropertyAnimation(this, "viewY");
scroller->setEasingCurve(QEasingCurve(QEasingCurve::Linear));
// watch the view for mouse events
view->setMouseTracking(true);
scene->installEventFilter(this);
// once all widgets created we can connect the signals
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
connect(scroller, SIGNAL(finished()), this, SLOT(scrollFinished()));
connect(scrollbar, SIGNAL(valueChanged(int)), this, SLOT(scrollbarMoved(int)));
// set the widgets etc
configChanged(CONFIG_APPEARANCE);
// we're ready to plot, but not configured
configured=false;
stale=true;
current=NULL;
}
// add the item
void
ChartSpace::addItem(int order, int column, int deep, ChartSpaceItem *item)
{
item->order= order;
item->column = column;
item->deep = deep;
items.append(item);
}
// when a ride is selected we need to notify all the ChartSpaceItems
void
ChartSpace::rideSelected(RideItem *item)
{
// don't plot when we're not visible, unless we have nothing plotted yet
if (!isVisible() && current != NULL && item != NULL) {
stale=true;
return;
}
// don't replot .. we already did this one
if (current == item && stale == false) {
return;
}
// profiling the code
//QTime timer;
//timer.start();
// ride item changed
foreach(ChartSpaceItem *ChartSpaceItem, items) ChartSpaceItem->setData(item);
// profiling the code
//qDebug()<<"took:"<<timer.elapsed();
// update
updateView();
// ok, remember we did this one
current = item;
stale=false;
}
void
ChartSpaceItem::setData(RideItem *item)
{
Q_UNUSED(item);
// ignored
}
void
ChartSpaceItem::setDrag(bool x)
{
if (drag == x) return; // unchanged
drag = x;
// hide stuff
dragChanged(drag);
if (!drag) geometryChanged();
}
void
ChartSpaceItem::geometryChanged()
{
itemGeometryChanged();
}
bool
ChartSpaceItem::sceneEvent(QEvent *event)
{
// skip whilst dragging and resizing
if (parent->state != ChartSpace::NONE) return false;
// repaint when mouse enters and leaves
if (event->type() == QEvent::GraphicsSceneHoverLeave ||
event->type() == QEvent::GraphicsSceneHoverEnter) {
// force repaint
update();
scene()->update();
// repaint when in the corner
} else if (event->type() == QEvent::GraphicsSceneHoverMove && inCorner() != incorner) {
incorner = inCorner();
update();
scene()->update();
}
return false;
}
bool
ChartSpaceItem::inCorner()
{
QPoint vpos = parent->view->mapFromGlobal(QCursor::pos());
QPointF spos = parent->view->mapToScene(vpos);
if (geometry().contains(spos.x(), spos.y())) {
if (spos.y() - geometry().top() < (ROWHEIGHT+40) &&
geometry().width() - (spos.x() - geometry().x()) < (ROWHEIGHT+40))
return true;
}
return false;
}
bool
ChartSpaceItem::underMouse()
{
QPoint vpos = parent->view->mapFromGlobal(QCursor::pos());
QPointF spos = parent->view->mapToScene(vpos);
if (geometry().contains(spos.x(), spos.y())) return true;
return false;
}
// ChartSpaceItems need to show they are in config mode
void
ChartSpaceItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opt, QWidget *widget) {
if (drag) painter->setBrush(QBrush(GColor(CPLOTMARKER)));
else painter->setBrush(GColor(CCARDBACKGROUND));
QPainterPath path;
path.addRoundedRect(QRectF(0,0,geometry().width(),geometry().height()), ROWHEIGHT/5, ROWHEIGHT/5);
painter->setPen(Qt::NoPen);
//painter->fillPath(path, brush.color());
painter->drawPath(path);
painter->setPen(GColor(CPLOTGRID));
//XXXpainter->drawLine(QLineF(0,ROWHEIGHT*2,geometry().width(),ROWHEIGHT*2));
//painter->fillRect(QRectF(0,0,geometry().width()+1,geometry().height()+1), brush);
//titlefont.setWeight(QFont::Bold);
if (GCColor::luminance(GColor(CCARDBACKGROUND)) < 127) painter->setPen(QColor(200,200,200));
else painter->setPen(QColor(70,70,70));
painter->setFont(parent->titlefont);
painter->drawText(QPointF(ROWHEIGHT /2.0f, QFontMetrics(parent->titlefont, parent->device()).height()), name);
// only paint contents if not dragging
if (drag) return;
// not dragging so we can get to work painting the rest
if (parent->state != ChartSpace::DRAG && underMouse()) {
if (inCorner()) {
// 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));
// 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)));
} else {
// 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)));
itemPaint(painter, opt, widget);
}
static bool ChartSpaceItemSort(const ChartSpaceItem* left, const ChartSpaceItem* right)
{
return (left->column < right->column ? true : (left->column == right->column && left->order < right->order ? true : false));
}
void
ChartSpace::updateGeometry()
{
bool animated=false;
// prevent a memory leak
group->stop();
delete group;
group = new QParallelAnimationGroup(this);
// order the items to their positions
qSort(items.begin(), items.end(), ChartSpaceItemSort);
int y=SPACING;
int maxy = y;
int column=-1;
int x=SPACING;
// just set their geometry for now, no interaction
for(int i=0; i<items.count(); i++) {
// don't show hidden
if (!items[i]->isVisible()) continue;
// move on to next column, check if first item too
if (items[i]->column > column) {
// once past the first column we need to update x
if (column >= 0) x+= columns[column] + SPACING;
int diff = items[i]->column - column - 1;
if (diff > 0) {
// there are empty columns so shift the cols to the right
// to the left to fill the gap left and all the column
// widths also need to move down too
for(int j=items[i]->column-1; j < 8; j++) columns[j]=columns[j+1];
for(int j=i; j<items.count();j++) items[j]->column -= diff;
}
y=SPACING; column = items[i]->column;
}
// set geometry
int ty = y;
int tx = x;
int twidth = columns[column];
int theight = items[i]->deep * ROWHEIGHT;
// make em smaller when configuring visual cue stolen from Windows Start Menu
int add = 0; //XXX PERFORMANCE ISSSE XXX (state == DRAG) ? (ROWHEIGHT/2) : 0;
// for setting the scene rectangle - but ignore a ChartSpaceItem if we are dragging it
if (maxy < ty+theight+SPACING) maxy = ty+theight+SPACING;
// add to scene if new
if (!items[i]->onscene) {
scene->addItem(items[i]);
items[i]->setGeometry(tx, ty, twidth, theight);
items[i]->onscene = true;
} else if (items[i]->invisible == false &&
(items[i]->geometry().x() != tx+add ||
items[i]->geometry().y() != ty+add ||
items[i]->geometry().width() != twidth-(add*2) ||
items[i]->geometry().height() != theight-(add*2))) {
// we've got an animation to perform
animated = true;
// add an animation for this movement
QPropertyAnimation *animation = new QPropertyAnimation(items[i], "geometry");
animation->setDuration(300);
animation->setStartValue(items[i]->geometry());
animation->setEndValue(QRect(tx+add,ty+add,twidth-(add*2),theight-(add*2)));
// when placing a little feedback helps
if (items[i]->placing) {
animation->setEasingCurve(QEasingCurve(QEasingCurve::OutBack));
items[i]->placing = false;
} else animation->setEasingCurve(QEasingCurve(QEasingCurve::OutQuint));
group->addAnimation(animation);
}
// set spot for next tile
y += theight + SPACING;
}
// set the scene rectangle, columns start at 0
sceneRect = QRectF(0, 0, columns[column] + x + SPACING, maxy);
if (animated) group->start();
}
void
ChartSpace::configChanged(qint32)
{
grayConfig = colouredIconFromPNG(":images/configure.png", GColor(COVERVIEWBACKGROUND).lighter(75));
whiteConfig = colouredIconFromPNG(":images/configure.png", QColor(100,100,100));
accentConfig = colouredIconFromPNG(":images/configure.png", QColor(150,150,150));
// set fonts
bigfont.setPixelSize(pixelSizeForFont(bigfont, ROWHEIGHT *2.5f));
titlefont.setPixelSize(pixelSizeForFont(titlefont, ROWHEIGHT)); // need a bit of space
midfont.setPixelSize(pixelSizeForFont(midfont, ROWHEIGHT *0.8f));
smallfont.setPixelSize(pixelSizeForFont(smallfont, ROWHEIGHT*0.7f));
setProperty("color", GColor(COVERVIEWBACKGROUND));
view->setBackgroundBrush(QBrush(GColor(COVERVIEWBACKGROUND)));
scene->setBackgroundBrush(QBrush(GColor(COVERVIEWBACKGROUND)));
scrollbar->setStyleSheet(TabView::ourStyleSheet());
// text edit colors
QPalette palette;
palette.setColor(QPalette::Window, GColor(COVERVIEWBACKGROUND));
palette.setColor(QPalette::Background, GColor(COVERVIEWBACKGROUND));
// only change base if moved away from white plots
// which is a Mac thing
#ifndef Q_OS_MAC
if (GColor(COVERVIEWBACKGROUND) != Qt::white)
#endif
{
//palette.setColor(QPalette::Base, GCColor::alternateColor(GColor(CTRAINPLOTBACKGROUND)));
palette.setColor(QPalette::Base, GColor(COVERVIEWBACKGROUND));
palette.setColor(QPalette::Window, GColor(COVERVIEWBACKGROUND));
}
#ifndef Q_OS_MAC // the scrollers appear when needed on Mac, we'll keep that
//code->setStyleSheet(TabView::ourStyleSheet());
#endif
palette.setColor(QPalette::WindowText, GCColor::invertColor(GColor(COVERVIEWBACKGROUND)));
palette.setColor(QPalette::Text, GCColor::invertColor(GColor(COVERVIEWBACKGROUND)));
//code->setPalette(palette);
repaint();
}
void
ChartSpace::updateView()
{
scene->setSceneRect(sceneRect);
scene->update();
// don'r scale whilst resizing on x?
if (scrolling || (state != YRESIZE && state != XRESIZE && state != DRAG)) {
// much of a resize / change ?
double dx = fabs(viewRect.x() - sceneRect.x());
double dy = fabs(viewRect.y() - sceneRect.y());
double vy = fabs(viewRect.y()-double(_viewY));
double dwidth = fabs(viewRect.width() - sceneRect.width());
double dheight = fabs(viewRect.height() - sceneRect.height());
// scale immediately if not a bit change
// otherwise it feels unresponsive
if (viewRect.width() == 0 || (vy < 20 && dx < 20 && dy < 20 && dwidth < 20 && dheight < 20)) {
setViewRect(sceneRect);
} else {
// tempting to make this longer but feels ponderous at longer durations
viewchange->setDuration(400);
viewchange->setStartValue(viewRect);
viewchange->setEndValue(sceneRect);
viewchange->start();
}
}
if (view->sceneRect().height() >= scene->sceneRect().height()) {
scrollbar->setEnabled(false);
} else {
// now set scrollbar
setscrollbar = true;
scrollbar->setMinimum(0);
scrollbar->setMaximum(scene->sceneRect().height()-view->sceneRect().height());
scrollbar->setValue(_viewY);
scrollbar->setPageStep(view->sceneRect().height());
scrollbar->setEnabled(true);
setscrollbar = false;
}
}
void
ChartSpace::edgeScroll(bool down)
{
// already scrolling, so don't move
if (scrolling) return;
// we basically scroll the view if the cursor is at or above
// the top of the view, or at or below the bottom and the mouse
// is moving away. Needs to work in normal and full screen.
if (state == DRAG || state == YRESIZE) {
QPointF pos =this->mapFromGlobal(QCursor::pos());
if (!down && pos.y() <= 0) {
// at the top of the screen, go up a qtr of a screen
scrollTo(_viewY - (view->sceneRect().height()/4));
} else if (down && (geometry().height()-pos.y()) <= 0) {
// at the bottom of the screen, go down a qtr of a screen
scrollTo(_viewY + (view->sceneRect().height()/4));
}
}
}
void
ChartSpace::scrollTo(int newY)
{
// bound the target to the top or a screenful from the bottom, except when we're
// resizing on Y as we are expanding the scene by increasing the size of an object
if ((state != YRESIZE) && (newY +view->sceneRect().height()) > sceneRect.bottom())
newY = sceneRect.bottom() - view->sceneRect().height();
if (newY < 0)
newY = 0;
if (_viewY != newY) {
if (abs(_viewY - newY) < 20) {
// for small scroll increments just do it, its tedious to wait for animations
_viewY = newY;
updateView();
} else {
// disable other view updates whilst scrolling
scrolling = true;
// make it snappy for short distances - ponderous for drag scroll
// and vaguely snappy for page by page scrolling
if (state == DRAG || state == YRESIZE) scroller->setDuration(300);
else if (abs(_viewY-newY) < 100) scroller->setDuration(150);
else scroller->setDuration(250);
scroller->setStartValue(_viewY);
scroller->setEndValue(newY);
scroller->start();
}
}
}
void
ChartSpace::setViewRect(QRectF rect)
{
viewRect = rect;
// fit to scene width XXX need to fix scrollbars.
double scale = view->frameGeometry().width() / viewRect.width();
QRectF scaledRect(0,_viewY, viewRect.width(), view->frameGeometry().height() / scale);
// scale to selection
view->scale(scale,scale);
view->setSceneRect(scaledRect);
view->fitInView(scaledRect, Qt::KeepAspectRatio);
// if we're dragging, as the view changes it can be really jarring
// as the dragged item is not under the mouse then snaps back
// this might need to be cleaned up as a little too much of spooky
// action at a distance going on here !
if (state == DRAG) {
// update drag point
QPoint vpos = view->mapFromGlobal(QCursor::pos());
QPointF pos = view->mapToScene(vpos);
// move the ChartSpaceItem being dragged
stateData.drag.item->setPos(pos.x()-stateData.drag.offx, pos.y()-stateData.drag.offy);
}
view->update();
}
bool
ChartSpace::eventFilter(QObject *, QEvent *event)
{
if (block || (event->type() != QEvent::KeyPress && event->type() != QEvent::GraphicsSceneWheel &&
event->type() != QEvent::GraphicsSceneMousePress && event->type() != QEvent::GraphicsSceneMouseRelease &&
event->type() != QEvent::GraphicsSceneMouseMove)) {
return false;
}
block = true;
bool returning = false;
// we only filter out keyboard shortcuts for undo redo etc
// in the qwkcode editor, anything else is of no interest.
if (event->type() == QEvent::KeyPress) {
// we care about cmd / ctrl
Qt::KeyboardModifiers kmod = static_cast<QInputEvent*>(event)->modifiers();
bool ctrl = (kmod & Qt::ControlModifier) != 0;
switch(static_cast<QKeyEvent*>(event)->key()) {
case Qt::Key_Y:
if (ctrl) {
//workout->redo();
returning = true; // we grab all key events
}
break;
case Qt::Key_Z:
if (ctrl) {
//workout->undo();
returning=true;
}
break;
case Qt::Key_Home:
scrollTo(0);
break;
case Qt::Key_End:
scrollTo(scene->sceneRect().bottom());
break;
case Qt::Key_PageDown:
scrollTo(_viewY + view->sceneRect().height());
break;
case Qt::Key_PageUp:
scrollTo(_viewY - view->sceneRect().height());
break;
case Qt::Key_Down:
scrollTo(_viewY + ROWHEIGHT);
break;
case Qt::Key_Up:
scrollTo(_viewY - ROWHEIGHT);
break;
}
} else if (event->type() == QEvent::GraphicsSceneWheel) {
// take it as applied
QGraphicsSceneWheelEvent *w = static_cast<QGraphicsSceneWheelEvent*>(event);
scrollTo(_viewY - (w->delta()*2));
event->accept();
returning = true;
} else if (event->type() == QEvent::GraphicsSceneMousePress) {
// we will process clicks when configuring so long as we're
// not in the middle of something else - this is to start
// dragging a ChartSpaceItem around
if (state == NONE) {
// where am i ?
QPointF pos = static_cast<QGraphicsSceneMouseEvent*>(event)->scenePos();
QGraphicsItem *gritem = scene->itemAt(pos, view->transform());
ChartSpaceItem *item = static_cast<ChartSpaceItem*>(gritem);
// ignore other scene elements (e.g. charts)
if (!items.contains(item)) item=NULL;
// only respond to clicks not in config corner button
if (item && ! item->inCorner()) {
// are we on the boundary of the ChartSpaceItem?
double offx = pos.x()-item->geometry().x();
double offy = pos.y()-item->geometry().y();
if (item->geometry().height()-offy < 10) {
state = YRESIZE;
stateData.yresize.item = item;
stateData.yresize.deep = item->deep;
stateData.yresize.posy = pos.y();
// thanks we'll take that
event->accept();
returning = true;
} else if (item->geometry().width()-offx < 10) {
state = XRESIZE;
stateData.xresize.column = item->column;
stateData.xresize.width = columns[item->column];
stateData.xresize.posx = pos.x();
// thanks we'll take that
event->accept();
returning = true;
} else {
// we're grabbing a ChartSpaceItem, so lets
// work out the offset so we can move
// it around when we start dragging
state = DRAG;
item->invisible = true;
item->setDrag(true);
item->setZValue(100);
stateData.drag.item = item;
stateData.drag.offx = offx;
stateData.drag.offy = offy;
stateData.drag.width = columns[item->column];
// thanks we'll take that
event->accept();
returning = true;
// what is the offset?
//updateGeometry();
scene->update();
view->update();
}
}
}
} else if (event->type() == QEvent::GraphicsSceneMouseRelease) {
// stop dragging
if (state == DRAG || state == YRESIZE || state == XRESIZE) {
// we want this one
event->accept();
returning = true;
// set back to visible if dragging
if (state == DRAG) {
stateData.drag.item->invisible = false;
stateData.drag.item->setZValue(10);
stateData.drag.item->placing = true;
stateData.drag.item->setDrag(false);
}
// end state;
state = NONE;
// drop it down
updateGeometry();
updateView();
}
} else if (event->type() == QEvent::GraphicsSceneMouseMove) {
// where is the mouse now?
QPointF pos = static_cast<QGraphicsSceneMouseEvent*>(event)->scenePos();
// check for autoscrolling at edges
if (state == DRAG || state == YRESIZE) edgeScroll(lasty < pos.y());
// remember pos
lasty = pos.y();
if (state == NONE) { // hovering
// where am i ?
QGraphicsItem *gritem = scene->itemAt(pos, view->transform());
ChartSpaceItem *item = static_cast<ChartSpaceItem*>(gritem);
// ignore other scene elements (e.g. charts)
if (!items.contains(item)) item=NULL;
if (item) {
// are we on the boundary of the ChartSpaceItem?
double offx = pos.x()-item->geometry().x();
double offy = pos.y()-item->geometry().y();
if (yresizecursor == false && item->geometry().height()-offy < 10) {
yresizecursor = true;
setCursor(QCursor(Qt::SizeVerCursor));
} else if (yresizecursor == true && item->geometry().height()-offy > 10) {
yresizecursor = false;
setCursor(QCursor(Qt::ArrowCursor));
}
if (xresizecursor == false && item->geometry().width()-offx < 10) {
xresizecursor = true;
setCursor(QCursor(Qt::SizeHorCursor));
} else if (xresizecursor == true && item->geometry().width()-offx > 10) {
xresizecursor = false;
setCursor(QCursor(Qt::ArrowCursor));
}
} else {
// not hovering over tile, so if still have a resize cursor
// set it back to the normal arrow pointer
if (yresizecursor || xresizecursor || cursor().shape() != Qt::ArrowCursor) {
xresizecursor = yresizecursor = false;
setCursor(QCursor(Qt::ArrowCursor));
}
}
} else if (state == DRAG && !scrolling) { // dragging?
// whilst mouse moves, only update geom when changed
bool changed = false;
// move the ChartSpaceItem being dragged
stateData.drag.item->setPos(pos.x()-stateData.drag.offx, pos.y()-stateData.drag.offy);
// should I move?
QList<QGraphicsItem *> overlaps;
foreach(QGraphicsItem *p, scene->items(pos))
if(items.contains(static_cast<ChartSpaceItem*>(p)))
overlaps << p;
// we always overlap with ourself, so see if more
if (overlaps.count() > 1) {
ChartSpaceItem *over = static_cast<ChartSpaceItem*>(overlaps[1]);
if (pos.y()-over->geometry().y() > over->geometry().height()/2) {
// place below the one its over
stateData.drag.item->column = over->column;
stateData.drag.item->order = over->order+1;
for(int i=items.indexOf(over); i< items.count(); i++) {
if (i>=0 && items[i]->column == over->column && items[i]->order > over->order && items[i] != stateData.drag.item) {
items[i]->order += 1;
changed = true;
}
}
} else {
// place above the one its over
stateData.drag.item->column = over->column;
stateData.drag.item->order = over->order;
for(int i=0; i< items.count(); i++) {
if (i>=0 && items[i]->column == over->column && items[i]->order >= (over->order) && items[i] != stateData.drag.item) {
items[i]->order += 1;
changed = true;
}
}
}
} else {
// columns are now variable width
// create a new column to the right?
int x=SPACING;
int targetcol = -1;
for(int i=0; i<10; i++) {
if (pos.x() > x && pos.x() < (x+columns[i]+SPACING)) {
targetcol = i;
break;
}
x += columns[i]+SPACING;
}
if (items.last()->column < 9 && targetcol < 0) {
// don't keep moving - if we're already alone in column 0 then no move is needed
if (stateData.drag.item->column != 0 || (items.count()>1 && items[1]->column == 0)) {
// new col to left
for(int i=0; i< items.count(); i++) items[i]->column += 1;
stateData.drag.item->column = 0;
stateData.drag.item->order = 0;
// shift columns widths to the right
for(int i=9; i>0; i--) columns[i] = columns[i-1];
columns[0] = stateData.drag.width;
changed = true;
}
} else if (items.last()->column < 9 && items.last() && items.last()->column < targetcol) {
// new col to the right
stateData.drag.item->column = items.last()->column + 1;
stateData.drag.item->order = 0;
// make column width same as source width
columns[stateData.drag.item->column] = stateData.drag.width;
changed = true;
} else {
// add to the end of the column
int last = -1;
for(int i=0; i<items.count() && items[i]->column <= targetcol; i++) {
if (items[i]->column == targetcol) last=i;
}
// so long as its been dragged below the last entry on the column !
if (last >= 0 && pos.y() > items[last]->geometry().bottom()) {
stateData.drag.item->column = targetcol;
stateData.drag.item->order = items[last]->order+1;
}
changed = true;
}
}
if (changed) {
// drop it down
updateGeometry();
updateView();
}
} else if (state == YRESIZE) {
// resize in rows, so in 75px units
int addrows = (pos.y() - stateData.yresize.posy) / ROWHEIGHT;
int setdeep = stateData.yresize.deep + addrows;
//min height
if (setdeep < 5) setdeep=5; // min of 5 rows
stateData.yresize.item->deep = setdeep;
// drop it down
updateGeometry();
updateView();
} else if ( state == XRESIZE) {
// multiples of 50 (smaller than margin)
int addblocks = (pos.x() - stateData.xresize.posx) / 50;
int setcolumn = stateData.xresize.width + (addblocks * 50);
// min max width
if (setcolumn < 800) setcolumn = 800;
if (setcolumn > 4400) setcolumn = 4400;
columns[stateData.xresize.column] = setcolumn;
// animate
updateGeometry();
updateView();
}
}
block = false;
return returning;
}
void
ChartSpaceItem::clicked()
{
if (isVisible()) hide();
else show();
//if (brush.color() == GColor(CChartSpaceItemBACKGROUND)) brush.setColor(Qt::red);
//else brush.setColor(GColor(CChartSpaceItemBACKGROUND));
update(geometry());
}

237
src/Gui/ChartSpace.h Normal file
View File

@@ -0,0 +1,237 @@
/*
* Copyright (c) 2020 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
*/
#ifndef _GC_ChartSpace_h
#define _GC_ChartSpace_h 1
// basics
#include "GoldenCheetah.h"
#include "Settings.h"
#include "Units.h"
#include "Colors.h"
#include "Context.h"
#include "Athlete.h"
#include "RideItem.h"
// QGraphics
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsWidget>
#include <QGraphicsDropShadowEffect>
// qt
#include <QtGui>
#include <QScrollBar>
#include <QIcon>
#include <QTimer>
// geometry basics
#define SPACING 80
#define ROWHEIGHT 80
class ChartSpace;
// must be subclassed to add items to a ChartSpace
class ChartSpaceItem : public QGraphicsWidget
{
Q_OBJECT
public:
// When subclassing you must reimplement these
virtual void itemPaint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) =0;
virtual void itemGeometryChanged() =0;
virtual void setData(RideItem *item)=0;
// 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) {
setAutoFillBackground(false);
setFlags(flags() | QGraphicsItem::ItemClipsToShape); // don't paint outside the card
setAcceptHoverEvents(true);
setZValue(10);
// a sensible default?
type = 0;
delcounter=0;
// watch geom changes
connect(this, SIGNAL(geometryChanged()), SLOT(geometryChanged()));
}
// watch mouse enter/leave
bool sceneEvent(QEvent *event);
bool inCorner();
bool underMouse();
// keep track of reuse of xyseries and delete to
// try and minimise memory leak in Qt Chart
int delcounter;
// when dragging around
void setDrag(bool x);
virtual void dragChanged(bool) {}
// my parent
ChartSpace *parent;
// what to do if clicked XXX just a hack for now
void clicked();
// name
QString name;
// which column, sequence and size in rows
int column, order, deep;
bool onscene, placing, drag;
bool incorner;
bool invisible;
// base paint
void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);
public slots:
void geometryChanged();
};
class ChartSpace : public QWidget
{
Q_OBJECT
Q_PROPERTY(QRectF viewRect READ getViewRect WRITE setViewRect)
Q_PROPERTY(int viewY READ getViewY WRITE setViewY)
public:
ChartSpace(Context *context);
// current state for event processing
enum { NONE, DRAG, XRESIZE, YRESIZE } state;
// used by children
Context *context;
QGraphicsView *view;
QFont titlefont, bigfont, midfont, smallfont;
// to get paint device
QGraphicsView *device() { return view; }
const QList<ChartSpaceItem*> allItems() { return items; }
public slots:
// ride item changed
void rideSelected(RideItem *item);
// for smooth scrolling
void setViewY(int x) { if (_viewY != x) {_viewY =x; updateView();} }
int getViewY() const { return _viewY; }
// for smooth scaling
void setViewRect(QRectF);
QRectF getViewRect() const { return viewRect; }
// trap signals
void configChanged(qint32);
// scale on first show
void showEvent(QShowEvent *) { updateView(); }
void resizeEvent(QResizeEvent *) { updateView(); }
// scrolling
void edgeScroll(bool down);
void scrollTo(int y);
void scrollFinished() { scrolling = false; updateView(); }
void scrollbarMoved(int x) { if (!scrolling && !setscrollbar) { setViewY(x); }}
// set geometry on the widgets (size and pos)
void updateGeometry();
// set scale, zoom etc appropriately
void updateView();
// add a ChartSpaceItem to the view
void addItem(int row, int column, int deep, ChartSpaceItem *item);
protected:
// process events
bool eventFilter(QObject *, QEvent *event);
private:
// gui setup
QGraphicsScene *scene;
QScrollBar *scrollbar;
// for animating transitions
QParallelAnimationGroup *group;
QPropertyAnimation *viewchange;
QPropertyAnimation *scroller;
// scene and view
int _viewY;
QRectF sceneRect;
QRectF viewRect;
// content
QVector<int> columns; // column widths
QList<ChartSpaceItem*> items; // tiles
// state data
bool yresizecursor; // is the cursor set to resize?
bool xresizecursor; // is the cursor set to resize?
bool block; // block event processing
bool scrolling; // scrolling the view?
bool setscrollbar; // distinguish between user and program actions
double lasty; // to see if the mouse is moving up or down
union ChartSpaceState {
struct {
double offx, offy; // mouse grab position on card
ChartSpaceItem *item; // index of card in QList
int width; // how big was I when I started dragging?
} drag;
struct {
double posy;
int deep;
ChartSpaceItem *item;
} yresize;
struct {
double posx;
int width;
int column;
} xresize;
} stateData;
bool stale;
bool configured;
RideItem *current;
};
#endif // _GC_ChartSpace_h

View File

@@ -39,6 +39,7 @@
#include "RideEditor.h"
#include "RideNavigator.h"
#include "RideSummaryWindow.h"
#include "RideMapWindow.h"
#include "ScatterWindow.h"
#include "SummaryWindow.h"
#include "MetadataWindow.h"
@@ -57,7 +58,7 @@
#endif
#include "PlanningWindow.h"
#ifdef GC_HAVE_OVERVIEW
#include "OverviewWindow.h"
#include "Overview.h"
#endif
#include "UserChart.h"
// Not until v4.0

View File

@@ -632,12 +632,13 @@ greaterThan(QT_MAJOR_VERSION, 4) {
# qt charts is officially supported from QT5.8 or higher
# in 5.7 it is a tech preview and not always available
greaterThan(QT_MINOR_VERSION, 7) {
QT += charts opengl
# Dashboard uses qt charts, so needs at least Qt 5.7
DEFINES += GC_HAVE_OVERVIEW
HEADERS += Charts/OverviewWindow.h
SOURCES += Charts/OverviewWindow.cpp
HEADERS += Gui/ChartSpace.h Charts/OverviewItems.h Charts/Overview.h
SOURCES += Gui/ChartSpace.cpp Charts/OverviewItems.cpp Charts/Overview.cpp
# generic chart
DEFINES += GC_HAVE_GENERIC