stella/src/gui/TimeMachineDialog.cxx

568 lines
17 KiB
C++

//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2022 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//============================================================================
#include "Dialog.hxx"
#include "Font.hxx"
#include "EventHandler.hxx"
#include "FrameBuffer.hxx"
#include "FBSurface.hxx"
#include "OSystem.hxx"
#include "Widget.hxx"
#include "StateManager.hxx"
#include "RewindManager.hxx"
#include "TimeLineWidget.hxx"
#include "TIASurface.hxx"
#include "Console.hxx"
#include "TIA.hxx"
#include "System.hxx"
#include "TimeMachineDialog.hxx"
#include "Base.hxx"
using Common::Base;
static constexpr int BUTTON_W = 14, BUTTON_H = 14;
static constexpr std::array<uInt32, BUTTON_H> RECORD = {
0b00000111100000,
0b00011111111000,
0b00111111111100,
0b01111111111110,
0b01111111111110,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b01111111111110,
0b01111111111110,
0b00111111111100,
0b00011111111000,
0b00000111100000
};
static constexpr std::array<uInt32, BUTTON_H> STOP = {
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111
};
static constexpr std::array<uInt32, BUTTON_H> EXIT = {
0b01100000000110,
0b11110000001111,
0b11111000011111,
0b01111100111110,
0b00111111111100,
0b00011111111000,
0b00001111110000,
0b00001111110000,
0b00011111111000,
0b00111111111100,
0b01111100111110,
0b11111000011111,
0b11110000001111,
0b01100000000110
};
static constexpr std::array<uInt32, BUTTON_H> REWIND_ALL = {
0,
0b11000011000011,
0b11000111000111,
0b11001111001111,
0b11011111011111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11011111011111,
0b11001111001111,
0b11000111000111,
0b11000011000011,
0
};
static constexpr std::array<uInt32, BUTTON_H> REWIND_1 = {
0,
0b00000110001110,
0b00001110001110,
0b00011110001110,
0b00111110001110,
0b01111110001110,
0b11111110001110,
0b11111110001110,
0b01111110001110,
0b00111110001110,
0b00011110001110,
0b00001110001110,
0b00000110001110,
0
};
static constexpr std::array<uInt32, BUTTON_H> PLAYBACK = {
0b11000000000000,
0b11110000000000,
0b11111100000000,
0b11111111000000,
0b11111111110000,
0b11111111111100,
0b11111111111111,
0b11111111111111,
0b11111111111100,
0b11111111110000,
0b11111111000000,
0b11111100000000,
0b11110000000000,
0b11000000000000
};
static constexpr std::array<uInt32, BUTTON_H> UNWIND_1 = {
0,
0b01110001100000,
0b01110001110000,
0b01110001111000,
0b01110001111100,
0b01110001111110,
0b01110001111111,
0b01110001111111,
0b01110001111110,
0b01110001111100,
0b01110001111000,
0b01110001110000,
0b01110001100000,
0
};
static constexpr std::array<uInt32, BUTTON_H> UNWIND_ALL = {
0,
0b11000011000011,
0b11100011100011,
0b11110011110011,
0b11111011111011,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111111111111,
0b11111011111011,
0b11110011110011,
0b11100011100011,
0b11000011000011,
0
};
static constexpr std::array<uInt32, BUTTON_H> SAVE_ALL = {
0b00000111100000,
0b00000111100000,
0b00000111100000,
0b00000111100000,
0b11111111111111,
0b01111111111110,
0b00111111111100,
0b00011111111000,
0b00001111110000,
0b00000111100000,
0b00000011000000,
0b00000000000000,
0b11111111111111,
0b11111111111111,
};
static constexpr std::array<uInt32, BUTTON_H> LOAD_ALL = {
0b00000011000000,
0b00000111100000,
0b00001111110000,
0b00011111111000,
0b00111111111100,
0b01111111111110,
0b11111111111111,
0b00000111100000,
0b00000111100000,
0b00000111100000,
0b00000111100000,
0b00000000000000,
0b11111111111111,
0b11111111111111,
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TimeMachineDialog::TimeMachineDialog(OSystem& osystem, DialogContainer& parent,
int width)
: Dialog(osystem, parent)
{
const GUI::Font& font = instance().frameBuffer().font();
const int H_BORDER = 6, BUTTON_GAP = 4, V_BORDER = 4;
const int buttonWidth = BUTTON_W + 10,
buttonHeight = BUTTON_H + 10,
rowHeight = font.getLineHeight();
int xpos, ypos;
// Set real dimensions
_w = width; // Parent determines our width (based on window size)
_h = V_BORDER * 2 + rowHeight + std::max(buttonHeight + 2, rowHeight);
this->clearFlags(Widget::FLAG_CLEARBG); // does only work combined with blending (0..100)!
this->clearFlags(Widget::FLAG_BORDER);
this->setFlags(Widget::FLAG_NOBG);
xpos = H_BORDER;
ypos = V_BORDER;
// Add index info
myCurrentIdxWidget = new StaticTextWidget(this, font, xpos, ypos, "1000", TextAlign::Left, kBGColor);
myCurrentIdxWidget->setTextColor(kColorInfo);
myCurrentIdxWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_NOBG);
myLastIdxWidget = new StaticTextWidget(this, font, _w - H_BORDER - font.getStringWidth("1000"), ypos,
"1000", TextAlign::Right, kBGColor);
myLastIdxWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_NOBG);
myLastIdxWidget->setTextColor(kColorInfo);
// Add timeline
const uInt32 tl_h = myCurrentIdxWidget->getHeight() / 2 + 6,
tl_x = xpos + myCurrentIdxWidget->getWidth() + 8,
tl_y = ypos + (myCurrentIdxWidget->getHeight() - tl_h) / 2 - 1,
tl_w = myLastIdxWidget->getAbsX() - tl_x - 8;
myTimeline = new TimeLineWidget(this, font, tl_x, tl_y, tl_w, tl_h, "", 0, kTimeline);
myTimeline->setMinValue(0);
ypos += rowHeight;
// Add time info
int ypos_s = ypos + (buttonHeight - font.getFontHeight() + 1) / 2; // align to button vertical center
myCurrentTimeWidget = new StaticTextWidget(this, font, xpos, ypos_s, "00:00.00", TextAlign::Left, kBGColor);
myCurrentTimeWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_NOBG);
myCurrentTimeWidget->setTextColor(kColorInfo);
myLastTimeWidget = new StaticTextWidget(this, font, _w - H_BORDER - font.getStringWidth("00:00.00"), ypos_s,
"00:00.00", TextAlign::Right, kBGColor);
myLastTimeWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_NOBG);
myLastTimeWidget->setTextColor(kColorInfo);
xpos = myCurrentTimeWidget->getRight() + BUTTON_GAP * 4;
// Add buttons
myToggleWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight,
STOP.data(), BUTTON_W, BUTTON_H, kToggle);
myToggleWidget->setToolTip("Toggle Time Machine mode.");
xpos += buttonWidth + BUTTON_GAP;
myExitWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight,
EXIT.data(), BUTTON_W, BUTTON_H, kExit);
myExitWidget->setToolTip("Exit Time Machine dialog.");
xpos += buttonWidth + BUTTON_GAP * 4;
myRewindAllWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight,
REWIND_ALL.data(), BUTTON_W, BUTTON_H, kRewindAll);
xpos += buttonWidth + BUTTON_GAP;
myRewind1Widget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight,
REWIND_1.data(), BUTTON_W, BUTTON_H, kRewind1, true);
xpos += buttonWidth + BUTTON_GAP;
myPlayBackWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight,
PLAYBACK.data(), BUTTON_W, BUTTON_H, kPlayBack);
myPlayBackWidget->setToolTip("Start playback of Time Machine states.");
xpos += buttonWidth + BUTTON_GAP;
myUnwind1Widget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight,
UNWIND_1.data(), BUTTON_W, BUTTON_H, kUnwind1, true);
xpos += buttonWidth + BUTTON_GAP;
myUnwindAllWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight,
UNWIND_ALL.data(), BUTTON_W, BUTTON_H, kUnwindAll);
xpos = myUnwindAllWidget->getRight() + BUTTON_GAP * 4;
mySaveAllWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight,
SAVE_ALL.data(), BUTTON_W, BUTTON_H, kSaveAll);
mySaveAllWidget->setToolTip("Save all Time Machine states.");
xpos = mySaveAllWidget->getRight() + BUTTON_GAP;
myLoadAllWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight,
LOAD_ALL.data(), BUTTON_W, BUTTON_H, kLoadAll);
myLoadAllWidget->setToolTip("Load all Time Machine states.");
xpos = myLoadAllWidget->getRight() + BUTTON_GAP * 4;
// Add message
int mWidth = (myLastTimeWidget->getLeft() - xpos) / font.getMaxCharWidth();
const string blanks = " ";
myMessageWidget = new StaticTextWidget(this, font, xpos, ypos_s,
blanks.substr(0, mWidth),
TextAlign::Left, kBGColor);
myMessageWidget->setFlags(Widget::FLAG_CLEARBG | Widget::FLAG_NOBG);
myMessageWidget->setTextColor(kColorInfo);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TimeMachineDialog::setPosition()
{
// Place on the bottom of the screen, centered horizontally
const Common::Size& screen = instance().frameBuffer().screenSize();
const Common::Rect& dst = surface().dstRect();
surface().setDstPos((screen.w - dst.w()) >> 1, screen.h - dst.h() - 10);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TimeMachineDialog::loadConfig()
{
// Enable blending (only once is necessary)
if(!surface().attributes().blending)
{
surface().attributes().blending = true;
surface().attributes().blendalpha = 92;
surface().applyAttributes();
}
initBar();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TimeMachineDialog::handleKeyDown(StellaKey key, StellaMod mod, bool repeated)
{
// The following shortcuts duplicate the shortcuts in EventHandler
Event::Type event = instance().eventHandler().eventForKey(EventMode::kEmulationMode, key, mod);
switch(event)
{
case Event::ExitMode:
handleCommand(nullptr, kExit, 0, 0);
break;
case Event::Rewind1Menu:
handleCommand(nullptr, kRewind1, 0, 0);
break;
case Event::Rewind10Menu:
handleCommand(nullptr, kRewind10, 0, 0);
break;
case Event::RewindAllMenu:
handleCommand(nullptr, kRewindAll, 0, 0);
break;
case Event::Unwind1Menu:
handleCommand(nullptr, kUnwind1, 0, 0);
break;
case Event::Unwind10Menu:
handleCommand(nullptr, kUnwind10, 0, 0);
break;
case Event::UnwindAllMenu:
handleCommand(nullptr, kUnwindAll, 0, 0);
break;
case Event::LoadAllStates:
if(!repeated)
handleCommand(nullptr, kLoadAll, 0, 0);
break;
case Event::SaveAllStates:
if(!repeated)
handleCommand(nullptr, kSaveAll, 0, 0);
break;
// Hotkey only commands (no button available)
case Event::SaveState:
case Event::PreviousState:
case Event::NextState:
case Event::LoadState:
case Event::TakeSnapshot:
if(!repeated)
handleCommand(nullptr, event, 0, 0);
break;
default:
Dialog::handleKeyDown(key, mod, repeated);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TimeMachineDialog::handleKeyUp(StellaKey key, StellaMod mod)
{
// The following shortcuts duplicate the shortcuts in EventHandler
// Note: mode switches must happen in key UP
Event::Type event = instance().eventHandler().eventForKey(EventMode::kEmulationMode, key, mod);
if(event == Event::TogglePlayBackMode || key == KBDK_SPACE)
handleCommand(nullptr, kPlayBack, 0, 0);
else
Dialog::handleKeyUp(key, mod);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TimeMachineDialog::handleCommand(CommandSender* sender, int cmd,
int data, int id)
{
switch(cmd)
{
case kTimeline:
{
Int32 winds = myTimeline->getValue() -
instance().state().rewindManager().getCurrentIdx() + 1;
handleWinds(winds);
break;
}
case kToggle:
instance().toggleTimeMachine();
handleToggle();
break;
case kExit:
instance().eventHandler().leaveMenuMode();
break;
case kRewind1:
handleWinds(-1);
break;
case kRewind10:
handleWinds(-10);
break;
case kRewindAll:
handleWinds(-1000);
break;
case kPlayBack:
instance().eventHandler().enterPlayBackMode();
break;
case kUnwind1:
handleWinds(1);
break;
case kUnwind10:
handleWinds(10);
break;
case kUnwindAll:
handleWinds(1000);
break;
case kSaveAll:
instance().eventHandler().handleEvent(Event::SaveAllStates);
break;
case kLoadAll:
instance().eventHandler().handleEvent(Event::LoadAllStates);
initBar();
break;
// Hotkey only commands (no button available)
case Event::SaveState:
case Event::PreviousState:
case Event::NextState:
case Event::LoadState:
instance().eventHandler().handleEvent(Event::Type(cmd));
break;
case Event::TakeSnapshot:
instance().eventHandler().handleEvent(Event::TakeSnapshot);
instance().frameBuffer().setPendingRender();
break;
default:
Dialog::handleCommand(sender, cmd, data, 0);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TimeMachineDialog::initBar()
{
const RewindManager& r = instance().state().rewindManager();
IntArray cycles = r.cyclesList();
// Set range and intervals for timeline
uInt32 maxValue = cycles.size() > 1 ? uInt32(cycles.size() - 1) : 0;
myTimeline->setMaxValue(maxValue);
myTimeline->setStepValues(cycles);
myMessageWidget->setLabel("");
handleWinds(_enterWinds);
_enterWinds = 0;
handleToggle();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
string TimeMachineDialog::getTimeString(uInt64 cycles) const
{
const Int32 scanlines = std::max<Int32>(instance().console().tia().scanlinesLastFrame(), 240);
const bool isNTSC = scanlines <= 287;
const Int32 NTSC_FREQ = 1193182; // ~76*262*60
const Int32 PAL_FREQ = 1182298; // ~76*312*50
const Int32 freq = isNTSC ? NTSC_FREQ : PAL_FREQ; // = cycles/second
uInt32 minutes = uInt32(cycles / (freq * 60));
cycles -= minutes * (freq * 60);
uInt32 seconds = uInt32(cycles / freq);
cycles -= seconds * freq;
uInt32 frames = uInt32(cycles / (scanlines * 76));
stringstream time;
time << Common::Base::toString(minutes, Common::Base::Fmt::_10_02) << ":";
time << Common::Base::toString(seconds, Common::Base::Fmt::_10_02) << ".";
time << Common::Base::toString(frames, Common::Base::Fmt::_10_02);
return time.str();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TimeMachineDialog::handleWinds(Int32 numWinds)
{
RewindManager& r = instance().state().rewindManager();
if(numWinds)
{
uInt64 startCycles = r.getCurrentCycles();
if(numWinds < 0) r.rewindStates(-numWinds);
else if(numWinds > 0) r.unwindStates(numWinds);
uInt64 elapsed = instance().console().tia().cycles() - startCycles;
if(elapsed > 0)
{
string message = r.getUnitString(elapsed);
// TODO: add message text from addState()
myMessageWidget->setLabel((numWinds < 0 ? "(-" : "(+") + message + ")");
}
}
// Update time
myCurrentTimeWidget->setLabel(getTimeString(r.getCurrentCycles() - r.getFirstCycles()));
myLastTimeWidget->setLabel(getTimeString(r.getLastCycles() - r.getFirstCycles()));
myTimeline->setValue(r.getCurrentIdx()-1);
// Update index
myCurrentIdxWidget->setValue(r.getCurrentIdx());
myLastIdxWidget->setValue(r.getLastIdx());
// Enable/disable buttons
myRewindAllWidget->setEnabled(!r.atFirst());
myRewind1Widget->setEnabled(!r.atFirst());
myPlayBackWidget->setEnabled(!r.atLast());
myUnwindAllWidget->setEnabled(!r.atLast());
myUnwind1Widget->setEnabled(!r.atLast());
mySaveAllWidget->setEnabled(r.getLastIdx() != 0);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void TimeMachineDialog::handleToggle()
{
myToggleWidget->setBitmap(instance().state().mode() == StateManager::Mode::Off ?
RECORD.data() : STOP.data(), BUTTON_W, BUTTON_H);
}