diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index d6f46fc9..405889af 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -58,6 +58,7 @@ SET(cockatrice_SOURCES src/tab_supervisor.cpp src/tab_admin.cpp src/tab_userlists.cpp + src/replay_timeline_widget.cpp src/chatview.cpp src/userlist.cpp src/userinfobox.cpp @@ -128,6 +129,7 @@ SET(cockatrice_HEADERS src/tab_supervisor.h src/tab_admin.h src/tab_userlists.h + src/replay_timeline_widget.h src/chatview.h src/userlist.h src/userinfobox.h diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index dd7416de..c6bb75eb 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -38,6 +38,14 @@ resources/icon_conceded.svg resources/icon_player.svg resources/icon_spectator.svg + + resources/replay_start.svg + resources/replay_stop.svg + resources/replay_fastforward.svg + resources/replay_rewind.svg + resources/replay_toend.svg + resources/replay_tostart.svg + resources/replay_pause.svg resources/genders/male.svg resources/genders/female.svg diff --git a/cockatrice/resources/replay_fastforward.svg b/cockatrice/resources/replay_fastforward.svg new file mode 100644 index 00000000..f46a38c4 --- /dev/null +++ b/cockatrice/resources/replay_fastforward.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/cockatrice/resources/replay_pause.svg b/cockatrice/resources/replay_pause.svg new file mode 100644 index 00000000..89bf0b7e --- /dev/null +++ b/cockatrice/resources/replay_pause.svg @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/cockatrice/resources/replay_rewind.svg b/cockatrice/resources/replay_rewind.svg new file mode 100644 index 00000000..9700cc0a --- /dev/null +++ b/cockatrice/resources/replay_rewind.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/cockatrice/resources/replay_start.svg b/cockatrice/resources/replay_start.svg new file mode 100644 index 00000000..30af90d0 --- /dev/null +++ b/cockatrice/resources/replay_start.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/cockatrice/resources/replay_stop.svg b/cockatrice/resources/replay_stop.svg new file mode 100644 index 00000000..17da8995 --- /dev/null +++ b/cockatrice/resources/replay_stop.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/cockatrice/resources/replay_toend.svg b/cockatrice/resources/replay_toend.svg new file mode 100644 index 00000000..3f6b82b6 --- /dev/null +++ b/cockatrice/resources/replay_toend.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/cockatrice/resources/replay_tostart.svg b/cockatrice/resources/replay_tostart.svg new file mode 100644 index 00000000..9b8b0936 --- /dev/null +++ b/cockatrice/resources/replay_tostart.svg @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/cockatrice/src/replay_timeline_widget.cpp b/cockatrice/src/replay_timeline_widget.cpp new file mode 100644 index 00000000..552b3a35 --- /dev/null +++ b/cockatrice/src/replay_timeline_widget.cpp @@ -0,0 +1,96 @@ +#include "replay_timeline_widget.h" +#include +#include +#include +#include + +ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent) + : QWidget(parent), maxBinValue(1), maxTime(1), timeScaleFactor(1.0), currentTime(0), currentEvent(0) +{ + replayTimer = new QTimer(this); + connect(replayTimer, SIGNAL(timeout()), this, SLOT(replayTimerTimeout())); +} + +const int ReplayTimelineWidget::binLength = 5000; + +void ReplayTimelineWidget::setTimeline(const QList &_replayTimeline) +{ + replayTimeline = _replayTimeline; + histogram.clear(); + int binEndTime = binLength - 1; + int binValue = 0; + for (int i = 0; i < replayTimeline.size(); ++i) { + if (replayTimeline[i] > binEndTime) { + histogram.append(binValue); + if (binValue > maxBinValue) + maxBinValue = binValue; + while (replayTimeline[i] > binEndTime + binLength) { + histogram.append(0); + binEndTime += binLength; + } + binValue = 1; + binEndTime += binLength; + } else + ++binValue; + } + histogram.append(binValue); + if (!replayTimeline.isEmpty()) + maxTime = replayTimeline.last(); + + update(); +} + +void ReplayTimelineWidget::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + painter.drawRect(0, 0, width() - 1, height() - 1); + + qreal binWidth = (qreal) width() / histogram.size(); + QPainterPath path; + path.moveTo(0, height() - 1); + for (int i = 0; i < histogram.size(); ++i) + path.lineTo(round(i * binWidth), (height() - 1) * (1.0 - (qreal) histogram[i] / maxBinValue)); + path.lineTo(width() - 1, height() - 1); + path.lineTo(0, height() - 1); + painter.fillPath(path, Qt::black); + + const QColor barColor = QColor::fromHsv(120, 255, 255, 100); + painter.fillRect(0, 0, (width() - 1) * currentTime / maxTime, height() - 1, barColor); +} + +QSize ReplayTimelineWidget::sizeHint() const +{ + return QSize(-1, 50); +} + +void ReplayTimelineWidget::replayTimerTimeout() +{ + currentTime += 200; + while ((currentEvent < replayTimeline.size()) && (replayTimeline[currentEvent] < currentTime)) { + emit processNextEvent(); + ++currentEvent; + } + if (currentEvent == replayTimeline.size()) { + emit replayFinished(); + replayTimer->stop(); + } + + if (!(currentTime % 1000)) + update(); +} + +void ReplayTimelineWidget::setTimeScaleFactor(qreal _timeScaleFactor) +{ + timeScaleFactor = _timeScaleFactor; + replayTimer->setInterval(200 / timeScaleFactor); +} + +void ReplayTimelineWidget::startReplay() +{ + replayTimer->start(200 / timeScaleFactor); +} + +void ReplayTimelineWidget::stopReplay() +{ + replayTimer->stop(); +} diff --git a/cockatrice/src/replay_timeline_widget.h b/cockatrice/src/replay_timeline_widget.h new file mode 100644 index 00000000..d5fd0ce4 --- /dev/null +++ b/cockatrice/src/replay_timeline_widget.h @@ -0,0 +1,39 @@ +#ifndef REPLAY_TIMELINE_WIDGET +#define REPLAY_TIMELINE_WIDGET + +#include +#include + +class QPaintEvent; +class QTimer; + +class ReplayTimelineWidget : public QWidget { + Q_OBJECT +signals: + void processNextEvent(); + void replayFinished(); +private: + QTimer *replayTimer; + QList replayTimeline; + QList histogram; + static const int binLength; + int maxBinValue, maxTime; + qreal timeScaleFactor; + int currentTime; + int currentEvent; +private slots: + void replayTimerTimeout(); +public: + ReplayTimelineWidget(QWidget *parent = 0); + void setTimeline(const QList &_replayTimeline); + QSize sizeHint() const; + void setTimeScaleFactor(qreal _timeScaleFactor); + int getCurrentEvent() const { return currentEvent; } +public slots: + void startReplay(); + void stopReplay(); +protected: + void paintEvent(QPaintEvent *event); +}; + +#endif diff --git a/cockatrice/src/tab_game.cpp b/cockatrice/src/tab_game.cpp index a4c0bf4b..254f7d5c 100644 --- a/cockatrice/src/tab_game.cpp +++ b/cockatrice/src/tab_game.cpp @@ -6,6 +6,8 @@ #include #include #include +#include + #include "tab_game.h" #include "tab_supervisor.h" #include "cardinfowidget.h" @@ -26,6 +28,7 @@ #include "main.h" #include "settingscache.h" #include "carddatabase.h" +#include "replay_timeline_widget.h" #include #include "pending_command.h" @@ -195,7 +198,7 @@ void DeckViewContainer::setDeck(DeckList *deck) deckView->setDeck(deck); readyStartButton->setEnabled(true); } - +#include TabGame::TabGame(GameReplay *_replay) : Tab(0), hostId(-1), @@ -213,6 +216,24 @@ TabGame::TabGame(GameReplay *_replay) gameId = replay->game_info().game_id(); gameDescription = QString::fromStdString(replay->game_info().description()); + // Create list: event number -> time [ms] + // Distribute simultaneous events evenly across 1 second. + int lastEventTimestamp = -1; + const int eventCount = replay->event_list_size(); + for (int i = 0; i < eventCount; ++i) { + int j = i + 1; + while ((j < eventCount) && (replay->event_list(j).seconds_elapsed() == lastEventTimestamp)) + ++j; + + const int numberEventsThisSecond = j - i; + for (int k = 0; k < numberEventsThisSecond; ++k) + replayTimeline.append(replay->event_list(i + k).seconds_elapsed() * 1000 + (int) ((qreal) k / (qreal) numberEventsThisSecond * 1000)); + + if (j < eventCount) + lastEventTimestamp = replay->event_list(j).seconds_elapsed(); + i += numberEventsThisSecond - 1; + } + phasesToolbar = new PhasesToolbar; phasesToolbar->hide(); @@ -241,6 +262,40 @@ TabGame::TabGame(GameReplay *_replay) QWidget *messageLogLayoutWidget = new QWidget; messageLogLayoutWidget->setLayout(messageLogLayout); + timelineWidget = new ReplayTimelineWidget; + timelineWidget->setTimeline(replayTimeline); + connect(timelineWidget, SIGNAL(processNextEvent()), this, SLOT(replayNextEvent())); + connect(timelineWidget, SIGNAL(replayFinished()), this, SLOT(replayFinished())); + + replayToStartButton = new QToolButton; + replayToStartButton->setIconSize(QSize(32, 32)); + replayToStartButton->setIcon(QIcon(":/resources/replay_tostart.svg")); + connect(replayToStartButton, SIGNAL(clicked()), this, SLOT(replayToStartButtonClicked())); + replayStartButton = new QToolButton; + replayStartButton->setIconSize(QSize(32, 32)); + replayStartButton->setIcon(QIcon(":/resources/replay_start.svg")); + connect(replayStartButton, SIGNAL(clicked()), this, SLOT(replayStartButtonClicked())); + replayPauseButton = new QToolButton; + replayPauseButton->setIconSize(QSize(32, 32)); + replayPauseButton->setEnabled(false); + replayPauseButton->setIcon(QIcon(":/resources/replay_pause.svg")); + connect(replayPauseButton, SIGNAL(clicked()), this, SLOT(replayPauseButtonClicked())); + replayStopButton = new QToolButton; + replayStopButton->setIconSize(QSize(32, 32)); + replayStopButton->setEnabled(false); + replayStopButton->setIcon(QIcon(":/resources/replay_stop.svg")); + connect(replayStopButton, SIGNAL(clicked()), this, SLOT(replayStopButtonClicked())); + replayFastForwardButton = new QToolButton; + replayFastForwardButton->setIconSize(QSize(32, 32)); + replayFastForwardButton->setEnabled(false); + replayFastForwardButton->setIcon(QIcon(":/resources/replay_fastforward.svg")); + replayFastForwardButton->setCheckable(true); + connect(replayFastForwardButton, SIGNAL(toggled(bool)), this, SLOT(replayFastForwardButtonToggled(bool))); + replayToEndButton = new QToolButton; + replayToEndButton->setIconSize(QSize(32, 32)); + replayToEndButton->setIcon(QIcon(":/resources/replay_toend.svg")); + connect(replayStopButton, SIGNAL(clicked()), this, SLOT(replayToEndButtonClicked())); + splitter = new QSplitter(Qt::Vertical); splitter->addWidget(cardInfo); splitter->addWidget(playerListWidget); @@ -251,6 +306,19 @@ TabGame::TabGame(GameReplay *_replay) mainLayout->addLayout(deckViewContainerLayout, 10); mainLayout->addWidget(splitter); + QHBoxLayout *replayControlLayout = new QHBoxLayout; + replayControlLayout->addWidget(timelineWidget, 10); + replayControlLayout->addWidget(replayToStartButton); + replayControlLayout->addWidget(replayStartButton); + replayControlLayout->addWidget(replayPauseButton); + replayControlLayout->addWidget(replayStopButton); + replayControlLayout->addWidget(replayFastForwardButton); + replayControlLayout->addWidget(replayToEndButton); + + QVBoxLayout *superMainLayout = new QVBoxLayout; + superMainLayout->addLayout(mainLayout); + superMainLayout->addLayout(replayControlLayout); + aNextPhase = 0; aNextTurn = 0; aRemoveLocalArrows = 0; @@ -263,16 +331,11 @@ TabGame::TabGame(GameReplay *_replay) tabMenu->addAction(aLeaveGame); retranslateUi(); - setLayout(mainLayout); + setLayout(superMainLayout); splitter->restoreState(settingsCache->getTabGameSplitterSizes()); messageLog->logReplayStarted(gameId); - - gameTimer = new QTimer(this); - gameTimer->setInterval(1000); - connect(gameTimer, SIGNAL(timeout()), this, SLOT(nextReplayStep())); - gameTimer->start(); } TabGame::TabGame(TabSupervisor *_tabSupervisor, QList &_clients, const Event_GameJoined &event) @@ -461,15 +524,62 @@ void TabGame::closeRequest() actLeaveGame(); } -void TabGame::nextReplayStep() +void TabGame::replayNextEvent() { - if (replay->event_list_size() <= currentReplayStep) { - QMessageBox::information(this, "", "done"); - gameTimer->stop(); - return; - } - processGameEventContainer(replay->event_list(currentReplayStep), 0); - ++currentReplayStep; + processGameEventContainer(replay->event_list(timelineWidget->getCurrentEvent()), 0); +} + +void TabGame::replayFinished() +{ + replayStartButton->setEnabled(true); + replayPauseButton->setEnabled(false); + replayStopButton->setEnabled(false); + replayFastForwardButton->setEnabled(false); +} + +void TabGame::replayToStartButtonClicked() +{ + // XXX +} + +void TabGame::replayStartButtonClicked() +{ + replayStartButton->setEnabled(false); + replayPauseButton->setEnabled(true); + replayStopButton->setEnabled(true); + replayFastForwardButton->setEnabled(true); + + timelineWidget->startReplay(); +} + +void TabGame::replayPauseButtonClicked() +{ + replayStartButton->setEnabled(true); + replayPauseButton->setEnabled(false); + replayFastForwardButton->setEnabled(false); + + timelineWidget->stopReplay(); +} + +void TabGame::replayStopButtonClicked() +{ + replayStartButton->setEnabled(true); + replayPauseButton->setEnabled(false); + replayStopButton->setEnabled(false); + replayFastForwardButton->setEnabled(false); + + timelineWidget->stopReplay(); + // XXX to start +} + +void TabGame::replayFastForwardButtonToggled(bool checked) +{ + timelineWidget->setTimeScaleFactor(checked ? 10.0 : 1.0); +} + +void TabGame::replayToEndButtonClicked() +{ + // XXX } void TabGame::incrementGameTime() diff --git a/cockatrice/src/tab_game.h b/cockatrice/src/tab_game.h index 8678a316..8c40a6fe 100644 --- a/cockatrice/src/tab_game.h +++ b/cockatrice/src/tab_game.h @@ -18,11 +18,13 @@ class QSplitter; class QLabel; class QLineEdit; class QPushButton; +class QToolButton; class QMenu; class ZoneViewLayout; class ZoneViewWidget; class PhasesToolbar; class PlayerListWidget; +class ReplayTimelineWidget; class Response; class GameEventContainer; class GameEventContext; @@ -108,8 +110,13 @@ private: QStringList phasesList; int currentPhase; int activePlayer; + + // Replay related members GameReplay *replay; int currentReplayStep; + QList replayTimeline; + ReplayTimelineWidget *timelineWidget; + QToolButton *replayToStartButton, *replayStartButton, *replayPauseButton, *replayStopButton, *replayFastForwardButton, *replayToEndButton; QSplitter *splitter; CardInfoWidget *cardInfo; @@ -158,7 +165,15 @@ signals: void containerProcessingDone(); void openMessageDialog(const QString &userName, bool focus); private slots: - void nextReplayStep(); + void replayNextEvent(); + void replayFinished(); + void replayToStartButtonClicked(); + void replayStartButtonClicked(); + void replayPauseButtonClicked(); + void replayStopButtonClicked(); + void replayFastForwardButtonToggled(bool checked); + void replayToEndButtonClicked(); + void incrementGameTime(); void adminLockChanged(bool lock); void newCardAdded(AbstractCardItem *card);