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" |
35 | using Common::Base; |
36 | |
37 | const int BUTTON_W = 14, BUTTON_H = 14; |
38 | |
39 | static 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 | |
57 | static 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 | |
75 | static 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 | }; |
92 | static 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 | }; |
109 | static 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 | }; |
126 | static 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 | }; |
143 | static 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 | }; |
160 | static 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 | }; |
177 | static 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
196 | TimeMachineDialog::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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
283 | void 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
292 | void 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
306 | void 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
342 | void 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
403 | void 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
421 | string 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
444 | void 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 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
480 | void TimeMachineDialog::handleToggle() |
481 | { |
482 | myToggleWidget->setBitmap(instance().state().mode() == StateManager::Mode::Off ? RECORD : STOP, |
483 | BUTTON_W, BUTTON_H); |
484 | } |
485 | |