1//============================================================================
2//
3// SSSS tt lll lll
4// SS SS tt ll ll
5// SS tttttt eeee ll ll aaaa
6// SSSS tt ee ee ll ll aa
7// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
8// SS SS tt ee ll ll aa aa
9// SSSS ttt eeeee llll llll aaaaa
10//
11// Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony
12// and the Stella Team
13//
14// See the file "License.txt" for information on usage and redistribution of
15// this file, and for a DISCLAIMER OF ALL WARRANTIES.
16//============================================================================
17
18#include "Dialog.hxx"
19#include "Font.hxx"
20#include "EventHandler.hxx"
21#include "FrameBuffer.hxx"
22#include "FBSurface.hxx"
23#include "OSystem.hxx"
24#include "Widget.hxx"
25#include "StateManager.hxx"
26#include "RewindManager.hxx"
27#include "TimeLineWidget.hxx"
28
29#include "Console.hxx"
30#include "TIA.hxx"
31#include "System.hxx"
32
33#include "TimeMachineDialog.hxx"
34#include "Base.hxx"
35using Common::Base;
36
37const int BUTTON_W = 14, BUTTON_H = 14;
38
39static uInt32 RECORD[BUTTON_H] =
40{
41 0b00000111100000,
42 0b00011111111000,
43 0b00111111111100,
44 0b01111111111110,
45 0b01111111111110,
46 0b11111111111111,
47 0b11111111111111,
48 0b11111111111111,
49 0b11111111111111,
50 0b01111111111110,
51 0b01111111111110,
52 0b00111111111100,
53 0b00011111111000,
54 0b00000111100000
55};
56
57static uInt32 STOP[BUTTON_H] =
58{
59 0b11111111111111,
60 0b11111111111111,
61 0b11111111111111,
62 0b11111111111111,
63 0b11111111111111,
64 0b11111111111111,
65 0b11111111111111,
66 0b11111111111111,
67 0b11111111111111,
68 0b11111111111111,
69 0b11111111111111,
70 0b11111111111111,
71 0b11111111111111,
72 0b11111111111111
73};
74
75static uInt32 PLAY[BUTTON_H] =
76{
77 0b11000000000000,
78 0b11110000000000,
79 0b11111100000000,
80 0b11111111000000,
81 0b11111111110000,
82 0b11111111111100,
83 0b11111111111111,
84 0b11111111111111,
85 0b11111111111100,
86 0b11111111110000,
87 0b11111111000000,
88 0b11111100000000,
89 0b11110000000000,
90 0b11000000000000
91};
92static uInt32 REWIND_ALL[BUTTON_H] =
93{
94 0,
95 0b11000011000011,
96 0b11000111000111,
97 0b11001111001111,
98 0b11011111011111,
99 0b11111111111111,
100 0b11111111111111,
101 0b11111111111111,
102 0b11111111111111,
103 0b11011111011111,
104 0b11001111001111,
105 0b11000111000111,
106 0b11000011000011,
107 0
108};
109static uInt32 REWIND_1[BUTTON_H] =
110{
111 0,
112 0b00000110001110,
113 0b00001110001110,
114 0b00011110001110,
115 0b00111110001110,
116 0b01111110001110,
117 0b11111110001110,
118 0b11111110001110,
119 0b01111110001110,
120 0b00111110001110,
121 0b00011110001110,
122 0b00001110001110,
123 0b00000110001110,
124 0
125};
126static uInt32 UNWIND_1[BUTTON_H] =
127{
128 0,
129 0b01110001100000,
130 0b01110001110000,
131 0b01110001111000,
132 0b01110001111100,
133 0b01110001111110,
134 0b01110001111111,
135 0b01110001111111,
136 0b01110001111110,
137 0b01110001111100,
138 0b01110001111000,
139 0b01110001110000,
140 0b01110001100000,
141 0
142};
143static uInt32 UNWIND_ALL[BUTTON_H] =
144{
145 0,
146 0b11000011000011,
147 0b11100011100011,
148 0b11110011110011,
149 0b11111011111011,
150 0b11111111111111,
151 0b11111111111111,
152 0b11111111111111,
153 0b11111111111111,
154 0b11111011111011,
155 0b11110011110011,
156 0b11100011100011,
157 0b11000011000011,
158 0
159};
160static uInt32 SAVE_ALL[BUTTON_H] =
161{
162 0b00000111100000,
163 0b00000111100000,
164 0b00000111100000,
165 0b00000111100000,
166 0b11111111111111,
167 0b01111111111110,
168 0b00111111111100,
169 0b00011111111000,
170 0b00001111110000,
171 0b00000111100000,
172 0b00000011000000,
173 0b00000000000000,
174 0b11111111111111,
175 0b11111111111111,
176};
177static uInt32 LOAD_ALL[BUTTON_H] =
178{
179 0b00000011000000,
180 0b00000111100000,
181 0b00001111110000,
182 0b00011111111000,
183 0b00111111111100,
184 0b01111111111110,
185 0b11111111111111,
186 0b00000111100000,
187 0b00000111100000,
188 0b00000111100000,
189 0b00000111100000,
190 0b00000000000000,
191 0b11111111111111,
192 0b11111111111111,
193};
194
195// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
196TimeMachineDialog::TimeMachineDialog(OSystem& osystem, DialogContainer& parent,
197 int width)
198 : Dialog(osystem, parent),
199 _enterWinds(0)
200{
201 const GUI::Font& font = instance().frameBuffer().font();
202 const int H_BORDER = 6, BUTTON_GAP = 4, V_BORDER = 4;
203 const int buttonWidth = BUTTON_W + 10,
204 buttonHeight = BUTTON_H + 10,
205 rowHeight = font.getLineHeight();
206
207 int xpos, ypos;
208
209 // Set real dimensions
210 _w = width; // Parent determines our width (based on window size)
211 _h = V_BORDER * 2 + rowHeight + buttonHeight + 2;
212
213 this->clearFlags(Widget::FLAG_CLEARBG); // does only work combined with blending (0..100)!
214 this->clearFlags(Widget::FLAG_BORDER);
215
216 xpos = H_BORDER;
217 ypos = V_BORDER;
218
219 // Add index info
220 myCurrentIdxWidget = new StaticTextWidget(this, font, xpos, ypos, "1000", TextAlign::Left, kBGColor);
221 myCurrentIdxWidget->setTextColor(kColorInfo);
222 myLastIdxWidget = new StaticTextWidget(this, font, _w - H_BORDER - font.getStringWidth("1000"), ypos,
223 "1000", TextAlign::Right, kBGColor);
224 myLastIdxWidget->setTextColor(kColorInfo);
225
226 // Add timeline
227 const uInt32 tl_h = myCurrentIdxWidget->getHeight() / 2 + 6,
228 tl_x = xpos + myCurrentIdxWidget->getWidth() + 8,
229 tl_y = ypos + (myCurrentIdxWidget->getHeight() - tl_h) / 2 - 1,
230 tl_w = myLastIdxWidget->getAbsX() - tl_x - 8;
231 myTimeline = new TimeLineWidget(this, font, tl_x, tl_y, tl_w, tl_h, "", 0, kTimeline);
232 myTimeline->setMinValue(0);
233 ypos += rowHeight;
234
235 // Add time info
236 myCurrentTimeWidget = new StaticTextWidget(this, font, xpos, ypos + 3, "00:00.00", TextAlign::Left, kBGColor);
237 myCurrentTimeWidget->setTextColor(kColorInfo);
238 myLastTimeWidget = new StaticTextWidget(this, font, _w - H_BORDER - font.getStringWidth("00:00.00"), ypos + 3,
239 "00:00.00", TextAlign::Right, kBGColor);
240 myLastTimeWidget->setTextColor(kColorInfo);
241 xpos = myCurrentTimeWidget->getRight() + BUTTON_GAP * 4;
242
243 // Add buttons
244 myToggleWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, STOP,
245 BUTTON_W, BUTTON_H, kToggle);
246 xpos += buttonWidth + BUTTON_GAP;
247
248 myPlayWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, PLAY,
249 BUTTON_W, BUTTON_H, kPlay);
250 xpos += buttonWidth + BUTTON_GAP * 4;
251
252 myRewindAllWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, REWIND_ALL,
253 BUTTON_W, BUTTON_H, kRewindAll);
254 xpos += buttonWidth + BUTTON_GAP;
255
256 myRewind1Widget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, REWIND_1,
257 BUTTON_W, BUTTON_H, kRewind1, true);
258 xpos += buttonWidth + BUTTON_GAP;
259
260 myUnwind1Widget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, UNWIND_1,
261 BUTTON_W, BUTTON_H, kUnwind1, true);
262 xpos += buttonWidth + BUTTON_GAP;
263
264 myUnwindAllWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, UNWIND_ALL,
265 BUTTON_W, BUTTON_H, kUnwindAll);
266 xpos = myUnwindAllWidget->getRight() + BUTTON_GAP * 4;
267
268 mySaveAllWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, SAVE_ALL,
269 BUTTON_W, BUTTON_H, kSaveAll);
270 xpos = mySaveAllWidget->getRight() + BUTTON_GAP;
271
272 myLoadAllWidget = new ButtonWidget(this, font, xpos, ypos, buttonWidth, buttonHeight, LOAD_ALL,
273 BUTTON_W, BUTTON_H, kLoadAll);
274 xpos = myLoadAllWidget->getRight() + BUTTON_GAP * 4;
275
276 // Add message
277 myMessageWidget = new StaticTextWidget(this, font, xpos, ypos + 3, " ",
278 TextAlign::Left, kBGColor);
279 myMessageWidget->setTextColor(kColorInfo);
280}
281
282// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
283void TimeMachineDialog::center()
284{
285 // Place on the bottom of the screen, centered horizontally
286 const Common::Size& screen = instance().frameBuffer().screenSize();
287 const Common::Rect& dst = surface().dstRect();
288 surface().setDstPos((screen.w - dst.w()) >> 1, screen.h - dst.h() - 10);
289}
290
291// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
292void TimeMachineDialog::loadConfig()
293{
294 // Enable blending (only once is necessary)
295 if(!surface().attributes().blending)
296 {
297 surface().attributes().blending = true;
298 surface().attributes().blendalpha = 92;
299 surface().applyAttributes();
300 }
301
302 initBar();
303}
304
305// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
306void TimeMachineDialog::handleKeyDown(StellaKey key, StellaMod mod, bool repeated)
307{
308 // The following 'Alt' shortcuts duplicate the shortcuts in EventHandler
309 // It is best to keep them the same, so changes in EventHandler mean we
310 // need to update the logic here too
311 if(StellaModTest::isAlt(mod))
312 {
313 switch(key)
314 {
315 case KBDK_LEFT: // Alt-left(-shift) rewinds 1(10) states
316 handleCommand(nullptr, StellaModTest::isShift(mod) ? kRewind10 : kRewind1, 0, 0);
317 break;
318
319 case KBDK_RIGHT: // Alt-right(-shift) unwinds 1(10) states
320 handleCommand(nullptr, StellaModTest::isShift(mod) ? kUnwind10 : kUnwind1, 0, 0);
321 break;
322
323 case KBDK_DOWN: // Alt-down rewinds to start of list
324 handleCommand(nullptr, kRewindAll, 0, 0);
325 break;
326
327 case KBDK_UP: // Alt-up rewinds to end of list
328 handleCommand(nullptr, kUnwindAll, 0, 0);
329 break;
330
331 default:
332 Dialog::handleKeyDown(key, mod);
333 }
334 }
335 else if(key == KBDK_SPACE || key == KBDK_ESCAPE)
336 handleCommand(nullptr, kPlay, 0, 0);
337 else
338 Dialog::handleKeyDown(key, mod);
339}
340
341// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
342void TimeMachineDialog::handleCommand(CommandSender* sender, int cmd,
343 int data, int id)
344{
345 switch(cmd)
346 {
347 case kTimeline:
348 {
349 Int32 winds = myTimeline->getValue() -
350 instance().state().rewindManager().getCurrentIdx() + 1;
351 handleWinds(winds);
352 break;
353 }
354
355 case kToggle:
356 instance().state().toggleTimeMachine();
357 handleToggle();
358 break;
359
360 case kPlay:
361 instance().eventHandler().leaveMenuMode();
362 break;
363
364 case kRewind1:
365 handleWinds(-1);
366 break;
367
368 case kRewind10:
369 handleWinds(-10);
370 break;
371
372 case kRewindAll:
373 handleWinds(-1000);
374 break;
375
376 case kUnwind1:
377 handleWinds(1);
378 break;
379
380 case kUnwind10:
381 handleWinds(10);
382 break;
383
384 case kUnwindAll:
385 handleWinds(1000);
386 break;
387
388 case kSaveAll:
389 instance().frameBuffer().showMessage(instance().state().rewindManager().saveAllStates());
390 break;
391
392 case kLoadAll:
393 instance().frameBuffer().showMessage(instance().state().rewindManager().loadAllStates());
394 initBar();
395 break;
396
397 default:
398 Dialog::handleCommand(sender, cmd, data, 0);
399 }
400}
401
402// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
403void TimeMachineDialog::initBar()
404{
405 RewindManager& r = instance().state().rewindManager();
406 IntArray cycles = r.cyclesList();
407
408 // Set range and intervals for timeline
409 uInt32 maxValue = cycles.size() > 1 ? uInt32(cycles.size() - 1) : 0;
410 myTimeline->setMaxValue(maxValue);
411 myTimeline->setStepValues(cycles);
412
413 myMessageWidget->setLabel("");
414 handleWinds(_enterWinds);
415 _enterWinds = 0;
416
417 handleToggle();
418}
419
420// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
421string TimeMachineDialog::getTimeString(uInt64 cycles)
422{
423 const Int32 scanlines = std::max(instance().console().tia().scanlinesLastFrame(), 240u);
424 const bool isNTSC = scanlines <= 287;
425 const Int32 NTSC_FREQ = 1193182; // ~76*262*60
426 const Int32 PAL_FREQ = 1182298; // ~76*312*50
427 const Int32 freq = isNTSC ? NTSC_FREQ : PAL_FREQ; // = cycles/second
428
429 uInt32 minutes = uInt32(cycles / (freq * 60));
430 cycles -= minutes * (freq * 60);
431 uInt32 seconds = uInt32(cycles / freq);
432 cycles -= seconds * freq;
433 uInt32 frames = uInt32(cycles / (scanlines * 76));
434
435 stringstream time;
436 time << Common::Base::toString(minutes, Common::Base::F_10_02) << ":";
437 time << Common::Base::toString(seconds, Common::Base::F_10_02) << ".";
438 time << Common::Base::toString(frames, Common::Base::F_10_02);
439
440 return time.str();
441}
442
443// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
444void TimeMachineDialog::handleWinds(Int32 numWinds)
445{
446 RewindManager& r = instance().state().rewindManager();
447
448 if(numWinds)
449 {
450 uInt64 startCycles = r.getCurrentCycles();
451 if(numWinds < 0) r.rewindStates(-numWinds);
452 else if(numWinds > 0) r.unwindStates(numWinds);
453
454 uInt64 elapsed = instance().console().tia().cycles() - startCycles;
455 if(elapsed > 0)
456 {
457 string message = r.getUnitString(elapsed);
458
459 // TODO: add message text from addState()
460 myMessageWidget->setLabel((numWinds < 0 ? "(-" : "(+") + message + ")");
461 }
462 }
463
464 // Update time
465 myCurrentTimeWidget->setLabel(getTimeString(r.getCurrentCycles() - r.getFirstCycles()));
466 myLastTimeWidget->setLabel(getTimeString(r.getLastCycles() - r.getFirstCycles()));
467 myTimeline->setValue(r.getCurrentIdx()-1);
468 // Update index
469 myCurrentIdxWidget->setValue(r.getCurrentIdx());
470 myLastIdxWidget->setValue(r.getLastIdx());
471 // Enable/disable buttons
472 myRewindAllWidget->setEnabled(!r.atFirst());
473 myRewind1Widget->setEnabled(!r.atFirst());
474 myUnwindAllWidget->setEnabled(!r.atLast());
475 myUnwind1Widget->setEnabled(!r.atLast());
476 mySaveAllWidget->setEnabled(r.getLastIdx() != 0);
477}
478
479// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
480void TimeMachineDialog::handleToggle()
481{
482 myToggleWidget->setBitmap(instance().state().mode() == StateManager::Mode::Off ? RECORD : STOP,
483 BUTTON_W, BUTTON_H);
484}
485