traintastic/client/src/board/boardareawidget.cpp
2021-06-28 23:36:16 +02:00

430 Zeilen
13 KiB
C++

/**
* client/src/board/boardareawidget.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2020-2021 Reinder Feenstra
*
* 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 "boardareawidget.hpp"
#include <cmath>
#include <QPainter>
#include <QPaintEvent>
#include <QtMath>
#include <QApplication>
#include "boardwidget.hpp"
#include "tilepainter.hpp"
#include "../network/board.hpp"
#include "../network/abstractproperty.hpp"
#include "../network/abstractvectorproperty.hpp"
#include "../utils/rectf.hpp"
QRect rectToViewport(const QRect& r, const int gridSize)
{
QRect viewport;
viewport.setLeft((r.left() / gridSize) * gridSize);
viewport.setTop((r.top() / gridSize) * gridSize);
viewport.setRight(((r.right() + gridSize - 1) / gridSize) * gridSize);
viewport.setBottom(((r.bottom() + gridSize - 1) / gridSize) * gridSize);
return viewport;
}
// excludes grid/border
constexpr QRectF drawTileRect(const int x, const int y, const int w, const int h, const int tileSize)
{
return QRectF(x * (tileSize - 1), y * (tileSize - 1), 1 + w * (tileSize - 1), 1 + h * (tileSize - 1));
}
// includes grid/border
constexpr QRect updateTileRect(const int x, const int y, const int w, const int h, const int tileSize)
{
return QRect(x * (tileSize - 1) - 1, y * (tileSize - 1) - 1, 3 + w * (tileSize - 1), 3 + h * (tileSize - 1));
}
BoardAreaWidget::BoardAreaWidget(BoardWidget& board, QWidget* parent) :
QWidget(parent),
m_board{board},
m_boardLeft{board.board().getProperty("left")},
m_boardTop{board.board().getProperty("top")},
m_boardRight{board.board().getProperty("right")},
m_boardBottom{board.board().getProperty("bottom")},
m_grid{Grid::Dot},
m_zoomLevel{0},
m_mouseMoveTileId{TileId::None},
m_mouseMoveTileRotate{TileRotate::Deg0}
{
setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
setFocusPolicy(Qt::StrongFocus);
if(Q_LIKELY(m_boardLeft))
connect(m_boardLeft, &AbstractProperty::valueChanged, this, &BoardAreaWidget::updateMinimumSize);
if(Q_LIKELY(m_boardTop))
connect(m_boardTop, &AbstractProperty::valueChanged, this, &BoardAreaWidget::updateMinimumSize);
if(Q_LIKELY(m_boardRight))
connect(m_boardRight, &AbstractProperty::valueChanged, this, &BoardAreaWidget::updateMinimumSize);
if(Q_LIKELY(m_boardBottom))
connect(m_boardBottom, &AbstractProperty::valueChanged, this, &BoardAreaWidget::updateMinimumSize);
for(const auto& [l, object] : m_board.board().tileObjects())
tileObjectAdded(l.x, l.y, object);
updateMinimumSize();
}
void BoardAreaWidget::tileObjectAdded(int16_t x, int16_t y, const ObjectPtr& object)
{
const TileLocation l{x, y};
AbstractProperty* property;
if((property = object->getProperty("state")) || // block or sensor
(property = object->getProperty("position")) || // turnout
(property = object->getProperty("aspect"))) // signal
connect(property, &AbstractProperty::valueChanged, this,
[this, l]()
{
try
{
const TileData& tileData = m_board.board().tileData().at(l);
update(updateTileRect(l.x - boardLeft(), l.y - boardTop(), tileData.width(), tileData.height(), getTileSize()));
}
catch(...)
{
}
});
}
void BoardAreaWidget::setGrid(Grid value)
{
if(m_grid != value)
{
m_grid = value;
update();
emit gridChanged(m_grid);
}
}
void BoardAreaWidget::nextGrid()
{
switch(grid())
{
case Grid::None:
setGrid(Grid::Dot);
break;
case Grid::Dot:
setGrid(Grid::Line);
break;
case Grid::Line:
setGrid(Grid::None);
break;
}
}
void BoardAreaWidget::setZoomLevel(int value)
{
value = std::clamp(value, zoomLevelMin, zoomLevelMax);
if(m_zoomLevel != value)
{
m_zoomLevel = value;
updateMinimumSize();
update();
emit zoomLevelChanged(m_zoomLevel);
}
}
void BoardAreaWidget::setMouseMoveTileId(TileId id)
{
if(m_mouseMoveTileId == id)
return;
m_mouseMoveTileId = id;
update();
}
void BoardAreaWidget::setMouseMoveTileRotate(TileRotate rotate)
{
if(m_mouseMoveTileRotate == rotate)
return;
m_mouseMoveTileRotate = rotate;
update();
}
TurnoutPosition BoardAreaWidget::getTurnoutPosition(const TileLocation& l) const
{
if(ObjectPtr object = m_board.board().getTileObject(l))
if(const auto* p = object->getProperty("position"))
return p->toEnum<TurnoutPosition>();
return TurnoutPosition::Unknown;
}
BlockState BoardAreaWidget::getBlockState(const TileLocation& l) const
{
if(ObjectPtr object = m_board.board().getTileObject(l))
if(const auto* p = object->getProperty("state"))
return p->toEnum<BlockState>();
return BlockState::Unknown;
}
std::vector<SensorState> BoardAreaWidget::getBlockSensorStates(const TileLocation& l) const
{
if(ObjectPtr object = m_board.board().getTileObject(l))
if(const auto* p = object->getVectorProperty("sensor_states"))
{
const int size = p->size();
std::vector<SensorState> sensorStates(static_cast<size_t>(size));
for(int i = 0; i < size; i++)
sensorStates[i] = p->getEnum<SensorState>(i);
return sensorStates;
}
return {};
}
SensorState BoardAreaWidget::getSensorState(const TileLocation& l) const
{
if(ObjectPtr object = m_board.board().getTileObject(l))
if(const auto* p = object->getProperty("state"))
return p->toEnum<SensorState>();
return SensorState::Unknown;
}
SignalAspect BoardAreaWidget::getSignalAspect(const TileLocation& l) const
{
if(ObjectPtr object = m_board.board().getTileObject(l))
if(const auto* p = object->getProperty("aspect"))
return p->toEnum<SignalAspect>();
return SignalAspect::Unknown;
}
TileLocation BoardAreaWidget::pointToTileLocation(const QPoint& p)
{
const int pxPerTile = getTileSize() - 1;
return TileLocation{static_cast<int16_t>(p.x() / pxPerTile + boardLeft()), static_cast<int16_t>(p.y() / pxPerTile + boardTop())};
}
void BoardAreaWidget::leaveEvent(QEvent* event)
{
m_mouseMoveTileLocation = TileLocation::invalid;
emit mouseTileLocationChanged(m_mouseMoveTileLocation.x, m_mouseMoveTileLocation.y);
if(m_mouseMoveTileId != TileId::None)
update();
QWidget::leaveEvent(event);
}
void BoardAreaWidget::keyPressEvent(QKeyEvent* event)
{
if(event->key() == Qt::Key_G && event->modifiers() == Qt::ControlModifier)
nextGrid();
else if(event->matches(QKeySequence::ZoomIn))
zoomIn();
else if(event->matches(QKeySequence::ZoomOut))
zoomOut();
else
QWidget::keyPressEvent(event);
}
void BoardAreaWidget::mousePressEvent(QMouseEvent* event)
{
if(event->button() == Qt::LeftButton)
{
m_mouseLeftButtonPressed = true;
m_mouseLeftButtonPressedTileLocation = pointToTileLocation(event->pos());
}
else if(event->button() == Qt::RightButton)
{
m_mouseRightButtonPressed = true;
m_mouseRightButtonPressedPoint = event->pos();
}
}
void BoardAreaWidget::mouseReleaseEvent(QMouseEvent* event)
{
if(m_mouseLeftButtonPressed && event->button() == Qt::LeftButton)
{
m_mouseLeftButtonPressed = false;
TileLocation tl = pointToTileLocation(event->pos());
if(m_mouseLeftButtonPressedTileLocation == tl) // click
emit tileClicked(tl.x, tl.y);
}
else if(m_mouseRightButtonPressed && event->button() == Qt::RightButton)
{
m_mouseRightButtonPressed = false;
if((event->pos() - m_mouseRightButtonPressedPoint).manhattanLength() < 5 || m_mouseMoveTileId != TileId::None)
emit rightClicked();
}
}
void BoardAreaWidget::mouseMoveEvent(QMouseEvent* event)
{
if(hasMouseTracking())
{
const TileLocation tl = pointToTileLocation(event->pos());
if(m_mouseMoveTileLocation != tl)
{
const TileLocation old = m_mouseMoveTileLocation;
m_mouseMoveTileLocation = tl;
emit mouseTileLocationChanged(tl.x, tl.y);
if(m_mouseMoveTileId != TileId::None)
{
const int width = 1; // currently always 1
const int height = 1; // currently always 1
const int tileSize = getTileSize();
update(updateTileRect(old.x, old.y, width, height, tileSize));
update(updateTileRect(tl.x, tl.y, width, height, tileSize));
}
}
}
QWidget::mouseMoveEvent(event);
}
void BoardAreaWidget::wheelEvent(QWheelEvent* event)
{
if(QApplication::keyboardModifiers() == Qt::ControlModifier && event->angleDelta().y() != 0)
{
if(event->angleDelta().y() < 0)
zoomOut();
else
zoomIn();
event->accept();
}
else
QWidget::wheelEvent(event);
}
void BoardAreaWidget::paintEvent(QPaintEvent* event)
{
const QColor backgroundColor{0x10, 0x10, 0x10};
const QColor backgroundColor50{0x10, 0x10, 0x10, 0x80};
const QColor gridColor{0x40, 0x40, 0x40};
const QColor gridColorHighlight{Qt::white};
const int tileSize = getTileSize();
const int gridSize = tileSize - 1;
QPainter painter(this);
const QRect viewport = rectToViewport(event->rect(), gridSize);
painter.fillRect(viewport, backgroundColor);
// draw grid:
switch(m_grid)
{
case Grid::None:
break;
case Grid::Line:
painter.setPen(gridColor);
for(int y = viewport.top(); y <= viewport.bottom(); y += gridSize)
painter.drawLine(viewport.left(), y, viewport.right(), y);
for(int x = viewport.left(); x <= viewport.right(); x += gridSize)
painter.drawLine(x, viewport.top(), x, viewport.bottom());
break;
case Grid::Dot:
painter.setPen(gridColor);
for(int y = viewport.top(); y <= viewport.bottom(); y += gridSize)
for(int x = viewport.left(); x <= viewport.right(); x += gridSize)
painter.drawPoint(x, y);
break;
}
painter.setRenderHint(QPainter::Antialiasing, true);
// draw tiles:
TilePainter tilePainter{painter, tileSize};
const int tileOriginX = boardLeft();
const int tileOriginY = boardTop();
const QRect tiles{tileOriginX + viewport.left() / gridSize, tileOriginY + viewport.top() / gridSize, viewport.width() / gridSize, viewport.height() / gridSize};
painter.save();
for(auto it : m_board.board().tileData())
if(it.first.x + it.second.width() - 1 >= tiles.left() && it.first.x <= tiles.right() &&
it.first.y + it.second.height() - 1 >= tiles.top() && it.first.y <= tiles.bottom())
{
const TileId id = it.second.id();
const TileRotate a = it.second.rotate();
painter.setBrush(Qt::NoBrush);
const QRectF r = drawTileRect(it.first.x - tileOriginX, it.first.y - tileOriginY, it.second.width(), it.second.height(), tileSize);
switch(id)
{
case TileId::RailStraight:
case TileId::RailCurve45:
case TileId::RailCurve90:
case TileId::RailCross45:
case TileId::RailCross90:
case TileId::RailBufferStop:
tilePainter.draw(id, r, a);
break;
case TileId::RailTurnoutLeft45:
case TileId::RailTurnoutLeft90:
case TileId::RailTurnoutLeftCurved:
case TileId::RailTurnoutRight45:
case TileId::RailTurnoutRight90:
case TileId::RailTurnoutRightCurved:
case TileId::RailTurnoutWye:
case TileId::RailTurnout3Way:
case TileId::RailTurnoutSingleSlip:
case TileId::RailTurnoutDoubleSlip:
tilePainter.drawTurnout(id, r, a, getTurnoutPosition(it.first));
break;
case TileId::RailSensor:
tilePainter.drawSensor(id, r, a, getSensorState(it.first));
break;
case TileId::RailSignal2Aspect:
case TileId::RailSignal3Aspect:
tilePainter.drawSignal(id, r, a, getSignalAspect(it.first));
break;
case TileId::RailBlock:
tilePainter.drawBlock(id, r, a, getBlockState(it.first), getBlockSensorStates(it.first));
break;
case TileId::None:
case TileId::ReservedForFutureExpension:
break;
}
}
painter.restore();
if(m_mouseMoveTileId != TileId::None && m_mouseMoveTileLocation.isValid())
{
const QRectF r = drawTileRect(m_mouseMoveTileLocation.x - tileOriginX, m_mouseMoveTileLocation.y - tileOriginY, 1, 1, tileSize);
painter.fillRect(r, backgroundColor50);
painter.setPen(gridColorHighlight);
painter.drawRect(r.adjusted(-0.5, -0.5, 0.5, 0.5));
tilePainter.draw(m_mouseMoveTileId, r, m_mouseMoveTileRotate);
}
}
void BoardAreaWidget::updateMinimumSize()
{
const int tileSize = getTileSize() - 1;
const int width = 1 + tileSize * (1 + boardRight() - boardLeft());
const int height = 1 + tileSize * (1 + boardBottom() - boardTop());
setMinimumSize(width, height);
update();
}