/** * client/src/board/boardwidget.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 "boardwidget.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../mainwindow.hpp" #include "../network/board.hpp" #include "../network/connection.hpp" #include "../network/property.hpp" #include "../network/callmethod.hpp" #include "../theme/theme.hpp" struct TileInfo { QString classId; TileId id; uint8_t rotates; }; const std::array tileInfo = { TileInfo{QStringLiteral("board_tile.rail.straight"), TileId::RailStraight, 0xFF}, TileInfo{QStringLiteral("board_tile.rail.buffer_stop"), TileId::RailBufferStop, 0xFF}, TileInfo{QStringLiteral(""), TileId::None, 0}, TileInfo{QStringLiteral("board_tile.rail.curve_45"), TileId::RailCurve45, 0xFF}, TileInfo{QStringLiteral("board_tile.rail.curve_90"), TileId::RailCurve90, 0xFF}, TileInfo{QStringLiteral(""), TileId::None, 0}, TileInfo{QStringLiteral("board_tile.rail.cross_45"), TileId::RailCross45, 0x03}, TileInfo{QStringLiteral("board_tile.rail.cross_90"), TileId::RailCross90, 0x03}, TileInfo{QStringLiteral(""), TileId::None, 0}, TileInfo{QStringLiteral("board_tile.rail.turnout_left_45"), TileId::RailTurnoutLeft45, 0xFF}, TileInfo{QStringLiteral("board_tile.rail.turnout_left_90"), TileId::RailTurnoutLeft90, 0xFF}, TileInfo{QStringLiteral("board_tile.rail.turnout_left_curved"), TileId::RailTurnoutLeftCurved, 0xFF}, TileInfo{QStringLiteral("board_tile.rail.turnout_right_45"), TileId::RailTurnoutRight45, 0xFF}, TileInfo{QStringLiteral("board_tile.rail.turnout_right_90"), TileId::RailTurnoutRight90, 0xFF}, TileInfo{QStringLiteral("board_tile.rail.turnout_right_curved"), TileId::RailTurnoutRightCurved, 0xFF}, TileInfo{QStringLiteral("board_tile.rail.turnout_wye"), TileId::RailTurnoutWye, 0xFF}, TileInfo{QStringLiteral("board_tile.rail.turnout_3way"), TileId::RailTurnout3Way, 0xFF}, TileInfo{QStringLiteral("board_tile.rail.turnout_singleslip"), TileId::RailTurnoutSingleSlip, 0xFF}, TileInfo{QStringLiteral("board_tile.rail.turnout_doubleslip"), TileId::RailTurnoutDoubleSlip, 0xFF}, TileInfo{QStringLiteral(""), TileId::None, 0}, TileInfo{QStringLiteral("board_tile.rail.block"), TileId::RailBlock, 0x05}, TileInfo{QStringLiteral("board_tile.rail.sensor"), TileId::RailSensor, 0xFF}, TileInfo{QStringLiteral(""), TileId::None, 0}, TileInfo{QStringLiteral("board_tile.rail.signal_2_aspect"), TileId::RailSignal2Aspect, 0xFF}, TileInfo{QStringLiteral("board_tile.rail.signal_3_aspect"), TileId::RailSignal3Aspect, 0xFF} }; inline void validRotate(TileRotate& rotate, uint8_t rotates) { Q_ASSERT(rotates != 0); while(((1 << static_cast(rotate)) & rotates) == 0) rotate += TileRotate::Deg45; } BoardWidget::BoardWidget(std::shared_ptr object, QWidget* parent) : QWidget(parent), m_object{std::move(object)}, m_boardArea{new BoardAreaWidget(*this, this)}, m_statusBar{new QStatusBar(this)}, m_statusBarMessage{new QLabel(this)}, m_statusBarCoords{new QLabel(this)}, m_editActions{new QActionGroup(this)}, m_editRotate{TileRotate::Deg0} { if(AbstractProperty* name = m_object->getProperty("name")) { connect(name, &AbstractProperty::valueChangedString, this, &BoardWidget::setWindowTitle); setWindowTitle(name->toString()); } QMenu* m; QVBoxLayout* l = new QVBoxLayout(); l->setMargin(0); // main toolbar: QToolBar* toolbar = new QToolBar(this); l->addWidget(toolbar); toolbar->addAction(/*Theme::getIcon("properties"),*/ Locale::tr("qtapp:board_properties"), []() { // TODO }); toolbar->addSeparator(); m_actionZoomIn = toolbar->addAction(Theme::getIcon("zoom_in"), Locale::tr("qtapp:zoom_in"), m_boardArea, &BoardAreaWidget::zoomIn); m_actionZoomOut = toolbar->addAction(Theme::getIcon("zoom_out"), Locale::tr("qtapp:zoom_out"), m_boardArea, &BoardAreaWidget::zoomOut); toolbar->addSeparator(); m_toolButtonGrid = new QToolButton(this); m_toolButtonGrid->setIcon(Theme::getIcon("grid_dot")); m_toolButtonGrid->setToolTip(Locale::tr("qtapp:grid")); m_toolButtonGrid->setPopupMode(QToolButton::MenuButtonPopup); connect(m_toolButtonGrid, &QToolButton::pressed, m_toolButtonGrid, &QToolButton::showMenu); m = new QMenu(this); m_actionGridNone = m->addAction(Theme::getIcon("grid_none"), Locale::tr("qtapp:grid_none"), [this]() { m_boardArea->setGrid(BoardAreaWidget::Grid::None); }); m_actionGridDot = m->addAction(Theme::getIcon("grid_dot"), Locale::tr("qtapp:grid_dot"), [this]() { m_boardArea->setGrid(BoardAreaWidget::Grid::Dot); }); m_actionGridLine = m->addAction(Theme::getIcon("grid_line"), Locale::tr("qtapp:grid_line"), [this]() { m_boardArea->setGrid(BoardAreaWidget::Grid::Line); }); m_toolButtonGrid->setMenu(m); toolbar->addWidget(m_toolButtonGrid); // edit toolbar: m_toolbarEdit = new QToolBar(this); l->addWidget(m_toolbarEdit); m_editActions->setExclusive(true); m_editActionNone = m_editActions->addAction(m_toolbarEdit->addAction(Theme::getIcon("mouse"), "", this, [this]() { actionSelected(nullptr); })); m_editActionNone->setCheckable(true); m_editActionNone->setData(-1); m_editActionMove = m_editActions->addAction(m_toolbarEdit->addAction(Theme::getIcon("move_tile"), Locale::tr("board:move_tile"), this, [this]() { actionSelected(nullptr); })); m_editActionMove->setCheckable(true); m_editActionMove->setData(-1); m_editActionMove->setEnabled(false); // todo: implement m_editActionDelete = m_editActions->addAction(m_toolbarEdit->addAction(Theme::getIcon("delete"), Locale::tr("board:delete_tile"), this, [this]() { actionSelected(nullptr); })); m_editActionDelete->setCheckable(true); m_editActionDelete->setData(-1); m_toolbarEdit->addSeparator(); { QVector actions; for(size_t i = 0; i < tileInfo.size(); i++) { const TileInfo& info = tileInfo[i]; if(info.classId.isEmpty()) // next item group { if(actions.isEmpty()) continue; else if(actions.length() == 1) { actions[0]->setCheckable(true); m_toolbarEdit->addAction(m_editActions->addAction(actions[0])); connect(actions[0], &QAction::triggered, this, [this, action=actions[0]]() { actionSelected(&tileInfo[action->data().toInt()]); }); } else // > 1 { QAction* action = m_editActions->addAction(m_toolbarEdit->addAction("")); if(auto* tb = dynamic_cast(m_toolbarEdit->widgetForAction(action))) tb->setPopupMode(QToolButton::MenuButtonPopup); QMenu* m = new QMenu(this); for(auto subAction : actions) { m->addAction(subAction); connect(subAction, &QAction::triggered, this, [this, action, subAction]() { action->setIcon(subAction->icon()); action->setText(subAction->text()); action->setData(subAction->data()); action->setChecked(true); actionSelected(&tileInfo[subAction->data().toInt()]); }); } action->setIcon(actions[0]->icon()); action->setText(actions[0]->text()); action->setData(actions[0]->data()); action->setMenu(m); action->setCheckable(true); connect(action, &QAction::triggered, this, [this, action]() { actionSelected(&tileInfo[action->data().toInt()]); }); } actions.clear(); } else { QAction* act = new QAction(QIcon(QString(":/dark/").append(info.classId).append("")), Locale::tr(QString("class_id:").append(info.classId))); act->setData(static_cast(i)); actions.append(act); } } } m_editActions->actions().first()->setChecked(true); QScrollArea* sa = new QScrollArea(this); sa->setWidget(m_boardArea); l->addWidget(sa); m_statusBar->addWidget(m_statusBarMessage, 1); m_statusBar->addWidget(m_statusBarCoords, 0); l->addWidget(m_statusBar); AbstractProperty* edit = m_object->connection()->world()->getProperty("edit"); worldEditChanged(Q_LIKELY(edit) && edit->toBool()); if(Q_LIKELY(edit)) connect(edit, &AbstractProperty::valueChangedBool, this, &BoardWidget::worldEditChanged); setLayout(l); m_object->getTileData(); connect(m_object.get(), &Board::tileDataChanged, this, [this](){ m_boardArea->update(); }); connect(m_boardArea, &BoardAreaWidget::gridChanged, this, &BoardWidget::gridChanged); connect(m_boardArea, &BoardAreaWidget::zoomLevelChanged, this, &BoardWidget::zoomLevelChanged); connect(m_boardArea, &BoardAreaWidget::tileClicked, this, &BoardWidget::tileClicked); connect(m_boardArea, &BoardAreaWidget::rightClicked, this, &BoardWidget::rightClicked); connect(m_boardArea, &BoardAreaWidget::mouseTileLocationChanged, this, [this](int16_t x, int16_t y) { m_statusBarCoords->setText(QString::number(x) + ", " + QString::number(y)); }); gridChanged(m_boardArea->grid()); zoomLevelChanged(m_boardArea->zoomLevel()); } void BoardWidget::worldEditChanged(bool value) { m_toolbarEdit->setVisible(value); m_statusBar->setVisible(value); m_boardArea->setMouseTracking(value); } void BoardWidget::gridChanged(BoardAreaWidget::Grid value) { switch(value) { case BoardAreaWidget::Grid::None: m_toolButtonGrid->setIcon(m_actionGridNone->icon()); break; case BoardAreaWidget::Grid::Dot: m_toolButtonGrid->setIcon(m_actionGridDot->icon()); break; case BoardAreaWidget::Grid::Line: m_toolButtonGrid->setIcon(m_actionGridLine->icon()); break; } } void BoardWidget::zoomLevelChanged(int value) { m_actionZoomIn->setEnabled(value < BoardAreaWidget::zoomLevelMax); m_actionZoomOut->setEnabled(value > BoardAreaWidget::zoomLevelMin); } void BoardWidget::tileClicked(int16_t x, int16_t y) { if(m_toolbarEdit->isVisible()) // edit mode { QAction* act = m_editActions->checkedAction(); if(!act) return; if(act == m_editActionNone) { auto it = m_object->tileData().find({x, y}); if(it != m_object->tileData().end()) if(ObjectPtr obj = m_object->getTileObject({x, y})) MainWindow::instance->showObject(obj); } else if(act == m_editActionMove) { } else if(act == m_editActionDelete) { m_object->deleteTile(x, y, [this](const bool& r, Message::ErrorCode ec) { }); } else // add { const QString& classId = tileInfo[act->data().toInt()].classId; const Qt::KeyboardModifiers kbMod = QApplication::keyboardModifiers(); if(kbMod == Qt::NoModifier || kbMod == Qt::ControlModifier) m_object->addTile(x, y, m_editRotate, classId, kbMod == Qt::ControlModifier, [this](const bool& r, Message::ErrorCode ec) { }); } } else { auto it = m_object->tileData().find({x, y}); if(it != m_object->tileData().end()) { if(ObjectPtr obj = m_object->getTileObject({x, y})) { if(isRailTurnout(it->second.id())) { if(auto* m = obj->getMethod("next_position")) callMethod(*m, nullptr, false); } else if(isRailSignal(it->second.id())) { if(auto* m = obj->getMethod("next_aspect")) callMethod(*m, nullptr, false); } } } } } void BoardWidget::rightClicked() { if(QAction* act = m_editActions->checkedAction()) if(int index = act->data().toInt(); index >= 0 && Q_LIKELY(index < tileInfo.size())) { m_editRotate += TileRotate::Deg45; validRotate(m_editRotate, tileInfo[index].rotates); m_boardArea->setMouseMoveTileRotate(m_editRotate); } } void BoardWidget::actionSelected(const TileInfo* tileInfo) { if(tileInfo) { validRotate(m_editRotate, tileInfo->rotates); m_boardArea->setMouseMoveTileRotate(m_editRotate); m_boardArea->setMouseMoveTileId(tileInfo->id); } else m_boardArea->setMouseMoveTileId(TileId::None); }