Files
GoldenCheetah/src/Charts/ChartBar.cpp
Mark Liversedge 125a92514c Fix ChartBar Remove Chart SEGV
.. context menu action and processing in event loop deletes the widgets
   whilst events are being processed for it. So in event processing we
   now return immediately after triggering context menus.
2020-06-05 10:05:33 +01:00

621 lines
18 KiB
C++

/*
* Copyright (c) 2011 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 "ChartBar.h"
#include "DiaryWindow.h"
#include "DiarySidebar.h"
#include "Context.h"
#include <QFontMetrics>
#ifdef Q_OS_MAC
static int spacing_=4;
#else
static int spacing_=4;
#endif
ChartBar::ChartBar(Context *context) : QWidget(context->mainWindow), context(context)
{
// left / right scroller icon
static QIcon leftIcon = iconFromPNG(":images/mac/left.png");
static QIcon rightIcon = iconFromPNG(":images/mac/right.png");
setContentsMargins(0,0,0,0);
// main layout
QHBoxLayout *mlayout = new QHBoxLayout(this);
mlayout->setSpacing(0);
mlayout->setContentsMargins(0,0,0,0);
// buttonBar Widget
buttonBar = new ButtonBar(this);
buttonBar->setContentsMargins(0,0,0,0);
QHBoxLayout *vlayout = new QHBoxLayout(buttonBar);
vlayout->setSpacing(0);
vlayout->setContentsMargins(0,0,0,0);
layout = new QHBoxLayout;
layout->setSpacing(2 *dpiXFactor);
layout->setContentsMargins(0,0,0,0);
vlayout->addLayout(layout);
vlayout->addStretch();
// scrollarea
scrollArea = new QScrollArea(this);
scrollArea->setAutoFillBackground(true);
scrollArea->setWidgetResizable(false);
scrollArea->setFrameStyle(QFrame::NoFrame);
scrollArea->setContentsMargins(0,0,0,0);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setWidget(buttonBar);
// scroll area turns it on .. we turn it off!
buttonBar->setAutoFillBackground(false);
// for scrolling buttonbar
anim = new QPropertyAnimation(buttonBar, "pos", this);
// scroller buttons
left = new QToolButton(this);
left->setStyleSheet("QToolButton { border: none; padding: 0px; }");
left->setAutoFillBackground(false);
left->setFixedSize(20*dpiXFactor,20*dpiYFactor);
left->setIcon(leftIcon);
left->setIconSize(QSize(20*dpiXFactor,20*dpiYFactor));
left->setFocusPolicy(Qt::NoFocus);
mlayout->addWidget(left);
connect(left, SIGNAL(clicked()), this, SLOT(scrollLeft()));
// menu bar in the middle of the buttons
mlayout->addWidget(scrollArea);
right = new QToolButton(this);
right->setStyleSheet("QToolButton { border: none; padding: 0px; }");
right->setAutoFillBackground(false);
right->setFixedSize(20*dpiXFactor,20*dpiYFactor);
right->setIcon(rightIcon);
right->setIconSize(QSize(20*dpiXFactor,20*dpiYFactor));
right->setFocusPolicy(Qt::NoFocus);
mlayout->addWidget(right);
connect(right, SIGNAL(clicked()), this, SLOT(scrollRight()));
// spacer to make the menuButton on the right
QLabel *spacer = new QLabel("", this);
spacer->setAutoFillBackground(false);
spacer->setFixedHeight(20*dpiYFactor);
spacer->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
mlayout->addWidget(spacer);
menuButton = new QToolButton(this);
menuButton->setStyleSheet("QToolButton { border: none; padding: 0px; }");
menuButton->setAutoFillBackground(false);
menuButton->setFixedSize(20*dpiXFactor,20*dpiYFactor);
menuButton->setIcon(iconFromPNG(":images/sidebar/extra.png"));
menuButton->setIconSize(QSize(10*dpiXFactor,10*dpiYFactor));
menuButton->setFocusPolicy(Qt::NoFocus);
mlayout->addWidget(menuButton);
//connect(p, SIGNAL(clicked()), action, SLOT(trigger()));
signalMapper = new QSignalMapper(this); // maps each option
connect(signalMapper, SIGNAL(mapped(int)), this, SLOT(clicked(int)));
menuMapper = new QSignalMapper(this); // maps each option
connect(menuMapper, SIGNAL(mapped(int)), this, SLOT(triggerContextMenu(int)));
barMenu = new QMenu("Add");
chartMenu = barMenu->addMenu(tr("Add Chart"));
barMenu->addAction(tr("Import Chart..."), context->mainWindow, SLOT(importChart()));
#ifdef GC_HAS_CLOUD_DB
barMenu->addAction(tr("Upload Chart..."), context->mainWindow, SLOT(exportChartToCloudDB()));
barMenu->addAction(tr("Download Chart..."), context->mainWindow, SLOT(addChartFromCloudDB()));
#endif
// menu
connect(menuButton, SIGNAL(clicked()), this, SLOT(menuPopup()));
connect(chartMenu, SIGNAL(aboutToShow()), this, SLOT(setChartMenu()));
connect(chartMenu, SIGNAL(triggered(QAction*)), context->mainWindow, SLOT(addChart(QAction*)));
// trap resize / mouse events
installEventFilter(this);
// appearance update
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
configChanged(0);
}
void
ChartBar::configChanged(qint32)
{
buttonFont = QFont();
QFontMetrics fs(buttonFont);
int height = (fs.height()+(spacing_*dpiXFactor));
setFixedHeight(height);
scrollArea->setFixedHeight(height);
buttonBar->setFixedHeight(height);
QColor col=GColor(CCHROME);
scrollArea->setStyleSheet(QString("QScrollArea { background: rgb(%1,%2,%3); }").arg(col.red()).arg(col.green()).arg(col.blue()));
foreach(ChartBarItem *b, buttons) {
int width = fs.width(b->text) + (60 * dpiXFactor);
if (width < (90*dpiXFactor)) width=90*dpiXFactor;
b->setFont(buttonFont);
b->setFixedWidth(width);
b->setFixedHeight(height);
}
}
void
ChartBar::addWidget(QString title)
{
ChartBarItem *newbutton = new ChartBarItem(this);
newbutton->setText(title);
newbutton->setFont(buttonFont);
// make the right size
QFontMetrics fontMetric(buttonFont);
int width = fontMetric.width(title) + (60 * dpiXFactor);
int height = (fontMetric.height()+(spacing_*dpiXFactor));
if (width < (90*dpiXFactor)) width=90*dpiXFactor;
newbutton->setFixedWidth(width);
newbutton->setFixedHeight(height);
// add to layout
layout->addWidget(newbutton);
buttons << newbutton;
// map signals
connect(newbutton, SIGNAL(clicked(bool)), signalMapper, SLOT(map()));
connect(newbutton, SIGNAL(contextMenu()), menuMapper, SLOT(map()));
signalMapper->setMapping(newbutton, buttons.count()-1);
menuMapper->setMapping(newbutton, buttons.count()-1);
newbutton->installEventFilter(this);
// tidy up scrollers etc
tidy(true);
}
void
ChartBar::setChartMenu()
{
context->mainWindow->setChartMenu(chartMenu);
}
void
ChartBar::triggerContextMenu(int index)
{
QPoint tl = buttons[index]->geometry().topLeft();
int x = scrollArea->widget()->mapToGlobal(tl).x();
emit contextMenu(index, x);
}
void
ChartBar::menuPopup()
{
// set the point for the menu and call below
barMenu->exec(this->mapToGlobal(QPoint(menuButton->pos().x(), menuButton->pos().y()+20)));
}
void
ChartBar::setText(int index, QString text)
{
buttons[index]->setText(text);
QFontMetrics fontMetric(buttonFont);
int width = fontMetric.width(text) + (60*dpiXFactor);
buttons[index]->setWidth(width < (90*dpiXFactor) ? (90*dpiXFactor) : width);
buttons[index]->update();
tidy(true); // still fit ?
}
// tidy up the scrollers on first show...
void
ChartBar::tidy(bool setwidth)
{
// resize to button widths + 2px spacing
if (setwidth) {
int width = 2*dpiXFactor;
foreach (ChartBarItem *button, buttons) {
width += button->geometry().width() + (2*dpiXFactor);
}
buttonBar->setFixedWidth(width);
}
if (buttonBar->width() > scrollArea->width()) {
left->show(); right->show();
} else {
left->hide(); right->hide();
}
}
bool
ChartBar::eventFilter(QObject *object, QEvent *e)
{
// show/hide scrollers on resize event
if (object == this && e->type() == QEvent::Resize) {
// we do NOT move the position, we just show/hide
// the left and right scrollers
tidy(false);
}
// showing us - tidy up
if (object == this && e->type() == QEvent::Show) {
tidy(false);
}
// enter/leave we can track approximate mouse position and decide
// if we want to 'autoscroll'
if (e->type() == QEvent::Leave || e->type() == QEvent::Enter) {
tidy(false); // tidy up anyway
}
return false;
}
void
ChartBar::scrollRight()
{
// old position and pos we want
QPoint opos = buttonBar->pos();
QPoint pos = opos;
// just do a 3rd at a time, so we can see it move
pos.setX(pos.x() - (scrollArea->geometry().width()/3));
// constrain to just fit
if (pos.x() + buttonBar->geometry().width() < scrollArea->geometry().width())
pos.setX(scrollArea->geometry().width() - buttonBar->geometry().width());
// animated scroll
anim->setDuration(400);
anim->setEasingCurve(QEasingCurve::InOutQuad);
anim->setStartValue(opos);
anim->setEndValue(pos);
anim->start();
}
void
ChartBar::scrollLeft()
{
// old and new position
QPoint opos = buttonBar->pos();
QPoint pos = opos;
// a 3rd at a time
pos.setX(pos.x() + (scrollArea->geometry().width()/3));
if (pos.x() > 0) pos.setX(0);
// animated scroll
anim->setDuration(400);
anim->setEasingCurve(QEasingCurve::InOutQuad);
anim->setStartValue(opos);
anim->setEndValue(pos);
anim->start();
}
void
ChartBar::clear()
{
foreach(ChartBarItem *button, buttons) {
layout->removeWidget(button);
delete button;
}
buttons.clear();
}
void
ChartBar::removeWidget(int index)
{
layout->removeWidget(buttons[index]);
delete buttons[index];
buttons.takeAt(index);
// reset mappings
for (int i=0; i<buttons.count(); i++) {
signalMapper->setMapping(buttons[i], i);
menuMapper->setMapping(buttons[i], i);
}
tidy(true);
}
void
ChartBar::setCurrentIndex(int index)
{
clicked(index);
}
void
ChartBar::clicked(int index)
{
setUpdatesEnabled(false);
// set selected
for(int i=0; i<buttons.count(); i++) {
buttons[i]->setChecked(i == index);
}
setUpdatesEnabled(true);
emit currentIndexChanged(index);
}
ChartBar::~ChartBar() { }
void
ChartBar::paintEvent (QPaintEvent *event)
{
// paint the darn thing!
paintBackground(event);
QWidget::paintEvent(event);
}
// paint is the same as sidebar
void
ChartBar::paintBackground(QPaintEvent *)
{
// setup a painter and the area to paint
QPainter painter(this);
painter.save();
QRect all(0,0,width(),height());
painter.fillRect(all, GColor(CCHROME));
painter.restore();
}
void
ButtonBar::paintEvent(QPaintEvent *event)
{
// paint the darn thing!
paintBackground(event);
QWidget::paintEvent(event);
}
// paint is the same as sidebar
void
ButtonBar::paintBackground(QPaintEvent *)
{
// setup a painter and the area to paint
QPainter painter(this);
painter.save();
QRect all(0,0,width(),height());
// linear gradients
QLinearGradient active = GCColor::linearGradient(23*dpiYFactor, true);
QLinearGradient inactive = GCColor::linearGradient(23*dpiYFactor, false);
// fill with a linear gradient
painter.setPen(Qt::NoPen);
painter.fillRect(all, QColor(Qt::white));
painter.fillRect(all, isActiveWindow() ? active : inactive);
if (!GCColor::isFlat()) {
QPen black(QColor(100,100,100,200));
painter.setPen(black);
painter.drawLine(0,height()-1, width()-1, height()-1);
QPen gray(QColor(230,230,230));
painter.setPen(gray);
painter.drawLine(0,0, width()-1, 0);
}
painter.restore();
}
ChartBarItem::ChartBarItem(ChartBar *chartbar) : QWidget(chartbar), chartbar(chartbar)
{
red = highlighted = checked = false;
state = Idle;
QFont font;
font.setPointSize(10);
setFont(font);
setMouseTracking(true);
}
void
ChartBarItem::paintEvent(QPaintEvent *)
{
if (state == Drag) return; // invisible when dragging
QPainter painter(this);
painter.save();
painter.setRenderHints(QPainter::Antialiasing|QPainter::TextAntialiasing, true);
// widget rectangle
QRectF body(0,0,width(), height());
painter.setClipRect(body);
painter.setPen(Qt::NoPen);
// background - chrome or slected colour
QBrush brush(GColor(CCHROME));
if (underMouse() && !checked) brush = QBrush(Qt::darkGray);
if (checked) brush = QBrush(GColor(CPLOTBACKGROUND));
painter.fillRect(body, brush);
// now paint the text
QPen pen(GCColor::invertColor(brush.color()));
painter.setPen(pen);
painter.drawText(body, text, Qt::AlignBottom | Qt::AlignCenter);
// draw the bar
if (checked) painter.fillRect(QRect(0,0,geometry().width(), 3*dpiXFactor), QBrush(GColor(CPLOTMARKER)));
// draw the menu indicator
if (underMouse() && checked) {
QPoint mouse = mapFromGlobal(QCursor::pos());
QBrush brush(Qt::darkGray);
painter.setPen (Qt :: NoPen);
// different color if under mouse
if (hotspot.contains(mouse)) brush.setColor(GColor(CPLOTMARKER));
painter.fillPath (triangle, brush);
}
painter.restore();
}
int
ChartBarItem::indexPos(int x)
{
// map via global coord
QPoint global = mapToGlobal(QPoint(x,0));
// where in the scrollarea's widget are we now?
// this is equivalent to chartbar->buttonbar->mapFromGlobal(..)
int cpos = chartbar->scrollArea->widget()->mapFromGlobal(global).x();
// work through the layout items to find which widgets
for(int i=0; i<chartbar->layout->count(); i++) {
QPoint center = chartbar->layout->itemAt(i)->geometry().center();
if (center.x() > cpos) return i;
}
return -1;
}
bool
ChartBarItem::event(QEvent *e)
{
// resize?
if (e->type() == QEvent::Resize) {
int startx = width() - (20*dpiXFactor);
int depth = height() / 4;
int starty = (height() / 2.0) - (depth/2) + 3*dpiXFactor; // middle, taking into account bar at top
int hs = 3 * dpiXFactor;
// set the triangle
hotspot.clear();
hotspot.moveTo (startx-hs, starty-hs);
hotspot.lineTo (startx+(hs*2)+(8*dpiXFactor), starty-hs);
hotspot.lineTo (startx+(hs*2)+(8*dpiXFactor), starty+depth+hs);
hotspot.lineTo (startx-hs, starty+depth+hs);
hotspot.lineTo (startx, starty);
triangle.clear();
triangle.moveTo (startx, starty);
triangle.lineTo (startx+(8*dpiXFactor), starty);
triangle.lineTo (startx+(4*dpiXFactor), starty+depth);
triangle.lineTo (startx, starty);
}
// entry / exit event repaint for hover color
if (e->type() == QEvent::Leave || e->type() == QEvent::Enter) {
update();
}
if (e->type() == QEvent::MouseButtonPress && underMouse()) {
// menu?
if (checked && hotspot.contains(mapFromGlobal(QCursor::pos()))) {
// menu activated
state = Idle;
emit contextMenu();
return true; // no more processing of this event please - lots of stuff happens off that menu !
} else {
// selected with a click (not release)
state = Click;
clickpos.setX(static_cast<QMouseEvent*>(e)->x());
clickpos.setY(static_cast<QMouseEvent*>(e)->y());
emit clicked(checked);
}
}
if (e->type() == QEvent::MouseButtonRelease) {
if (state == Drag) {
// finish dragging, so drop into where we moved it
delete dragging;
int index = chartbar->layout->indexOf(this);
if (index != originalindex) {
// even the button array is used to index by ChartBar::setText(..)
// and then signal mapped to the index being selected. oh my.
// bit naughty modding from child here, but no easy way around it.
ChartBarItem *me = chartbar->buttons.takeAt(originalindex);
chartbar->buttons.insert(index, me);
for (int i=0; i<chartbar->buttons.count(); i++) {
chartbar->signalMapper->setMapping(chartbar->buttons[i], i);
chartbar->menuMapper->setMapping(chartbar->buttons[i], i);
}
// tell homewindow
chartbar->itemMoved(index, originalindex);
}
update();
}
state = Idle;
}
if (e->type() == QEvent::MouseMove) {
if (state == Click) {
// enter drag state - moved mouse before releasing the button click
state = Drag;
originalindex = chartbar->layout->indexOf(this);
repaint();
dragging = new ChartBarItem(chartbar);
dragging->state = Clone;
dragging->text = text;
dragging->checked = checked;
dragging->setFixedWidth(geometry().width());
dragging->setFixedHeight(geometry().height());
QPoint newpos = chartbar->mapFromGlobal(static_cast<QMouseEvent*>(e)->globalPos());
dragging->move(QPoint(newpos.x()-clickpos.x(),0));
dragging->show();
} else if (state == Drag) {
// move the clone tab for visual feedback
QPoint newpos = chartbar->mapFromGlobal(static_cast<QMouseEvent*>(e)->globalPos());
dragging->move(QPoint(newpos.x()-clickpos.x(),0));
// where are we currently?
int cindex = chartbar->layout->indexOf(this);
// work out where we should have dragged to
int indexpos = indexPos(static_cast<QMouseEvent*>(e)->x());
// if moving left, just do it...
if (cindex > indexpos) {
QLayoutItem *me = chartbar->layout->takeAt(cindex);
chartbar->layout->insertItem(indexpos, me);
} else if (indexpos > (cindex+1)) {
QLayoutItem *me = chartbar->layout->takeAt(cindex);
chartbar->layout->insertItem(indexpos-1, me); // indexpos-1 because we just got removed
}
}
update(); // in case hovering over stuff
}
return QWidget::event(e);
}