Loading files
9ac9868b1This file is pinned to the selected release. Download the original to open it in your design tool.
/*
* LibrePCB - Professional EDA for everyone!
* Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
* https://librepcb.org/
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
/*******************************************************************************
* Includes
******************************************************************************/
#include "graphicsview.h"
#include "../graphics/graphicsscene.h"
#include "QtOpenGL"
#include "if_graphicsvieweventhandler.h"
#include "waitingspinnerwidget.h"
#include <librepcb/core/application.h>
#include <librepcb/core/utils/scopeguard.h>
#include <QtCore>
#include <QtOpenGLWidgets>
#include <QtWidgets>
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
namespace editor {
/*******************************************************************************
* Constructors / Destructor
******************************************************************************/
GraphicsView::GraphicsView(QWidget* parent,
IF_GraphicsViewEventHandler* eventHandler) noexcept
: QGraphicsView(parent),
mWaitingSpinnerWidget(new WaitingSpinnerWidget(this)),
mInfoBoxLabel(new QLabel(this)),
mEventHandlerObject(eventHandler),
mScene(nullptr),
mZoomAnimation(nullptr),
mUseOpenGl(false),
mPanningActive(false),
mPanningButton(Qt::NoButton),
mPressedMouseButtons(Qt::NoButton),
mIdleTimeMs(0) {
setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
setOptimizationFlags(QGraphicsView::DontSavePainterState);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
setSceneRect(-2000, -2000, 4000, 4000);
mWaitingSpinnerWidget->setColor(QColor(Qt::gray).lighter(120));
mWaitingSpinnerWidget->hide();
mInfoBoxLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
mInfoBoxLabel->setFont(Application::getDefaultMonospaceFont());
mInfoBoxLabel->setTextFormat(Qt::RichText);
mInfoBoxLabel->move(0, 0);
mInfoBoxLabel->hide();
setInfoBoxColors(Qt::white, Qt::black);
mZoomAnimation = new QVariantAnimation();
connect(mZoomAnimation, &QVariantAnimation::valueChanged, this,
&GraphicsView::zoomAnimationValueChanged);
QTimer* idleTimer = new QTimer(this);
connect(idleTimer, &QTimer::timeout, this, [this]() { mIdleTimeMs += 100; });
idleTimer->start(100);
viewport()->grabGesture(Qt::PinchGesture);
}
GraphicsView::~GraphicsView() noexcept {
delete mZoomAnimation;
mZoomAnimation = nullptr;
}
/*******************************************************************************
* Getters
******************************************************************************/
QRectF GraphicsView::getVisibleSceneRect() const noexcept {
return mapToScene(viewport()->rect()).boundingRect();
}
/*******************************************************************************
* Setters
******************************************************************************/
void GraphicsView::setSpinnerColor(const QColor& color) noexcept {
mWaitingSpinnerWidget->setColor(color.lighter(120));
}
void GraphicsView::setInfoBoxColors(const QColor& fill,
const QColor& text) noexcept {
mInfoBoxLabel->setStyleSheet(QString("QLabel {"
" background-color: %1;"
" border: none;"
" border-bottom-right-radius: 15px;"
" padding: 5px;"
" color: %2;"
"}")
.arg(fill.name(QColor::HexArgb))
.arg(text.name(QColor::HexArgb)));
}
void GraphicsView::setUseOpenGl(bool useOpenGl) noexcept {
if (useOpenGl != mUseOpenGl) {
if (useOpenGl) {
// Try to make schematics/boards looking good by choosing reasonable
// format options (with default options, it looks ugly).
QSurfaceFormat format = QSurfaceFormat::defaultFormat();
format.setDepthBufferSize(24);
format.setSamples(8);
format.setStencilBufferSize(8);
format.setSwapBehavior(QSurfaceFormat::DoubleBuffer);
QOpenGLWidget* viewport = new QOpenGLWidget();
viewport->setFormat(format);
setViewport(viewport);
} else {
setViewport(nullptr);
}
mUseOpenGl = useOpenGl;
}
viewport()->grabGesture(Qt::PinchGesture);
}
void GraphicsView::setScene(GraphicsScene* scene) noexcept {
if (mScene) mScene->removeEventFilter(this);
mScene = scene;
if (mScene) mScene->installEventFilter(this);
QGraphicsView::setScene(mScene);
}
void GraphicsView::setVisibleSceneRect(const QRectF& rect) noexcept {
fitInView(rect, Qt::KeepAspectRatio);
}
void GraphicsView::setInfoBoxText(const QString& text) noexcept {
mInfoBoxLabel->setText(text);
mInfoBoxLabel->resize(mInfoBoxLabel->sizeHint());
mInfoBoxLabel->setVisible(!text.isEmpty());
}
void GraphicsView::setEventHandlerObject(
IF_GraphicsViewEventHandler* eventHandler) noexcept {
mEventHandlerObject = eventHandler;
}
/*******************************************************************************
* General Methods
******************************************************************************/
Point GraphicsView::mapGlobalPosToScenePos(
const QPoint& globalPosPx) const noexcept {
QPoint localPosPx = mapFromGlobal(globalPosPx);
localPosPx.setX(qBound(0, localPosPx.x(), width())); // Clip to view
localPosPx.setY(qBound(0, localPosPx.y(), height())); // Clip to view
return Point::fromPx(mapToScene(localPosPx));
}
QPainterPath GraphicsView::calcPosWithTolerance(
const Point& pos, qreal multiplier) const noexcept {
const qreal tolerance = 5 * multiplier; // Screen pixel tolerance.
const QRectF deviceRect(-tolerance, -tolerance, 2 * tolerance, 2 * tolerance);
const QTransform t(transform().inverted());
const QRectF sceneRect(t.mapRect(deviceRect).translated(pos.toPxQPointF()));
QPainterPath path;
path.addEllipse(sceneRect);
return path;
}
void GraphicsView::handleMouseWheelEvent(
QGraphicsSceneWheelEvent* event) noexcept {
if (event->modifiers().testFlag(Qt::ShiftModifier)) {
// horizontal scrolling
horizontalScrollBar()->setValue(horizontalScrollBar()->value() -
event->delta());
} else if (event->modifiers().testFlag(Qt::ControlModifier)) {
if (event->orientation() == Qt::Horizontal) {
// horizontal scrolling
horizontalScrollBar()->setValue(horizontalScrollBar()->value() -
event->delta());
} else {
// vertical scrolling
verticalScrollBar()->setValue(verticalScrollBar()->value() -
event->delta());
}
} else {
// Zoom to mouse
qreal scaleFactor = qPow(sZoomStepFactor, event->delta() / qreal(120));
scale(scaleFactor, scaleFactor);
}
event->setAccepted(true);
}
/*******************************************************************************
* Public Slots
******************************************************************************/
void GraphicsView::zoomIn() noexcept {
if (!mScene) return;
scale(sZoomStepFactor, sZoomStepFactor);
mIdleTimeMs = 0;
}
void GraphicsView::zoomOut() noexcept {
if (!mScene) return;
scale(1 / sZoomStepFactor, 1 / sZoomStepFactor);
mIdleTimeMs = 0;
}
void GraphicsView::zoomAll() noexcept {
if (!mScene) return;
QRectF rect = mScene->itemsBoundingRect();
if (rect.isEmpty()) rect = QRectF(-100, -100, 200, 200);
qreal xMargins = rect.width() / 50;
qreal yMargins = rect.height() / 50;
rect.adjust(-xMargins, -yMargins, xMargins, yMargins);
zoomToRect(rect);
mIdleTimeMs = 0;
}
void GraphicsView::zoomToRect(const QRectF& rect) noexcept {
mZoomAnimation->setDuration(500);
mZoomAnimation->setEasingCurve(QEasingCurve::InOutCubic);
mZoomAnimation->setStartValue(getVisibleSceneRect());
mZoomAnimation->setEndValue(rect);
mZoomAnimation->start();
mIdleTimeMs = 0;
}
void GraphicsView::showWaitingSpinner() noexcept {
mWaitingSpinnerWidget->show();
}
void GraphicsView::hideWaitingSpinner() noexcept {
mWaitingSpinnerWidget->hide();
}
/*******************************************************************************
* Private Slots
******************************************************************************/
void GraphicsView::zoomAnimationValueChanged(const QVariant& value) noexcept {
if (value.canConvert<QRectF>())
fitInView(value.toRectF(), Qt::KeepAspectRatio); // zoom smoothly
}
/*******************************************************************************
* Inherited from QGraphicsView
******************************************************************************/
// It is not possible to process the wheel event in the `eventFilter` because
// `QGraphicsSceneWheelEvent` does not track the source of the wheel event.
void GraphicsView::wheelEvent(QWheelEvent* event) {
if (event->source() == Qt::MouseEventSynthesizedBySystem) {
QAbstractScrollArea::wheelEvent(event);
} else {
QGraphicsView::wheelEvent(event);
}
}
bool GraphicsView::eventFilter(QObject* obj, QEvent* event) {
auto resetIdleTime = scopeGuard([this]() { mIdleTimeMs = 0; });
switch (event->type()) {
case QEvent::Gesture: {
QGestureEvent* ge = dynamic_cast<QGestureEvent*>(event);
QPinchGesture* pinch_g =
dynamic_cast<QPinchGesture*>(ge->gesture(Qt::PinchGesture));
if (pinch_g) {
scale(pinch_g->scaleFactor(), pinch_g->scaleFactor());
return true;
}
break;
}
case QEvent::GraphicsSceneMousePress: {
QGraphicsSceneMouseEvent* e =
dynamic_cast<QGraphicsSceneMouseEvent*>(event);
Q_ASSERT(e);
if ((e->button() == Qt::MiddleButton) ||
(e->button() == Qt::RightButton)) {
mPanningButton = e->button();
mCursorBeforePanning = cursor();
setCursor(Qt::ClosedHandCursor);
} else if (mEventHandlerObject && (e->button() == Qt::LeftButton)) {
const GraphicsSceneMouseEvent gsme{
Point::fromPx(e->scenePos()),
Point::fromPx(e->buttonDownScenePos(Qt::LeftButton)),
e->buttons(),
e->modifiers(),
};
mEventHandlerObject->graphicsSceneLeftMouseButtonPressed(gsme);
}
mPressedMouseButtons = e->buttons();
return true;
}
case QEvent::GraphicsSceneMouseRelease: {
QGraphicsSceneMouseEvent* e =
dynamic_cast<QGraphicsSceneMouseEvent*>(event);
Q_ASSERT(e);
bool wasPanning = false;
if ((mPanningButton != Qt::NoButton) && (e->button() == mPanningButton)) {
const QPoint diff =
e->screenPos() - e->buttonDownScreenPos(mPanningButton);
wasPanning = diff.manhattanLength() > 10;
mPanningButton = Qt::NoButton;
setCursor(mCursorBeforePanning);
}
if ((!wasPanning) && mEventHandlerObject) {
const GraphicsSceneMouseEvent gsme{
Point::fromPx(e->scenePos()),
Point::fromPx(e->buttonDownScenePos(e->button())),
e->buttons(),
e->modifiers(),
};
if (e->button() == Qt::LeftButton) {
mEventHandlerObject->graphicsSceneLeftMouseButtonReleased(gsme);
} else if (e->button() == Qt::RightButton) {
mEventHandlerObject->graphicsSceneRightMouseButtonReleased(gsme);
}
}
mPressedMouseButtons = e->buttons();
return true;
}
case QEvent::GraphicsSceneMouseMove: {
QGraphicsSceneMouseEvent* e =
dynamic_cast<QGraphicsSceneMouseEvent*>(event);
Q_ASSERT(e);
if ((mPanningButton != Qt::NoButton) && (!mPanningActive)) {
QPoint diff = mapFromScene(e->scenePos()) -
mapFromScene(e->buttonDownScenePos(mPanningButton));
mPanningActive = true; // avoid recursive calls (=> stack overflow)
horizontalScrollBar()->setValue(horizontalScrollBar()->value() -
diff.x());
verticalScrollBar()->setValue(verticalScrollBar()->value() - diff.y());
mPanningActive = false;
}
emit cursorScenePositionChanged(Point::fromPx(e->scenePos()));
mPressedMouseButtons = e->buttons();
if (mEventHandlerObject) {
const GraphicsSceneMouseEvent gsme{
Point::fromPx(e->scenePos()),
Point::fromPx(e->buttonDownScenePos(Qt::LeftButton)),
e->buttons(),
e->modifiers(),
};
mEventHandlerObject->graphicsSceneMouseMoved(gsme);
}
return true;
}
case QEvent::GraphicsSceneMouseDoubleClick: {
QGraphicsSceneMouseEvent* e =
dynamic_cast<QGraphicsSceneMouseEvent*>(event);
Q_ASSERT(e);
if (mEventHandlerObject && (e->button() == Qt::LeftButton)) {
const GraphicsSceneMouseEvent gsme{
Point::fromPx(e->scenePos()),
Point::fromPx(e->buttonDownScenePos(Qt::LeftButton)),
e->buttons(),
e->modifiers(),
};
return mEventHandlerObject->graphicsSceneLeftMouseButtonDoubleClicked(
gsme);
}
break;
}
case QEvent::KeyPress: {
if (mEventHandlerObject) {
QKeyEvent* e = dynamic_cast<QKeyEvent*>(event);
Q_ASSERT(e);
const GraphicsSceneKeyEvent gske{
Qt::Key(e->key()),
e->modifiers(),
};
return mEventHandlerObject->graphicsSceneKeyPressed(gske);
}
break;
}
case QEvent::KeyRelease: {
if (mEventHandlerObject) {
QKeyEvent* e = dynamic_cast<QKeyEvent*>(event);
Q_ASSERT(e);
const GraphicsSceneKeyEvent gske{
Qt::Key(e->key()),
e->modifiers(),
};
return mEventHandlerObject->graphicsSceneKeyReleased(gske);
}
break;
}
case QEvent::GraphicsSceneWheel: {
if (!underMouse()) break;
handleMouseWheelEvent(dynamic_cast<QGraphicsSceneWheelEvent*>(event));
return true;
}
default:
// Unknown event -> do not count as activity.
resetIdleTime.dismiss();
break;
}
return QWidget::eventFilter(obj, event);
}
/*******************************************************************************
* End of File
******************************************************************************/
} // namespace editor
} // namespace librepcb