_reverse.tcl 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. namespace eval reverse {
  2. # Enable reverse if not yet enabled. As an optimization, also return
  3. # reverse-status info (so that caller doesn't have to query it again).
  4. proc auto_enable {} {
  5. set stat_dict [reverse status]
  6. if {[dict get $stat_dict status] eq "disabled"} {
  7. reverse start
  8. }
  9. return $stat_dict
  10. }
  11. set_help_text reverse_prev \
  12. {Go back in time to the previous 'snapshot'.
  13. A 'snapshot' is actually an internal implementation detail, but the\
  14. important thing for this command is that the further back in the past,\
  15. the less dense the snapshots are. So executing this command multiple times\
  16. will take successively bigger steps in the past. Going back to a snapshot\
  17. is also slightly faster than going back to an arbitrary point in time\
  18. (let's say going back a fixed amount of time).
  19. }
  20. proc reverse_prev {{minimum 1} {maximum 15}} {
  21. set stats [auto_enable]
  22. set snapshots [dict get $stats snapshots]
  23. set num_snapshots [llength $snapshots]
  24. if {$num_snapshots == 0} return
  25. set current [dict get $stats current]
  26. set upperTarget [expr {$current - $minimum}]
  27. set lowerTarget [expr {$current - $maximum}]
  28. # search latest snapshot that is still before upperTarget
  29. set i [expr {$num_snapshots - 1}]
  30. while {([lindex $snapshots $i] > $upperTarget) && ($i > 0)} {
  31. incr i -1
  32. }
  33. # but don't go below lowerTarget
  34. set t [lindex $snapshots $i]
  35. if {$t < $lowerTarget} {set t $lowerTarget}
  36. reverse goto $t
  37. }
  38. set_help_text reverse_next \
  39. {This is very much like 'reverse_prev', but instead it goes to the closest\
  40. snapshot in the future (if possible).
  41. }
  42. proc reverse_next {{minimum 0} {maximum 15}} {
  43. set stats [auto_enable]
  44. set snapshots [dict get $stats snapshots]
  45. set num_snapshots [llength $snapshots]
  46. if {$num_snapshots == 0} return
  47. set current [dict get $stats current]
  48. set lowerTarget [expr {$current + $minimum}]
  49. set upperTarget [expr {$current + $maximum}]
  50. # search first snapshot that is after lowerTarget
  51. lappend snapshots [dict get $stats end]
  52. set i 0
  53. while {($i < $num_snapshots) && ([lindex $snapshots $i] < $lowerTarget)} {
  54. incr i
  55. }
  56. if {$i < $num_snapshots} {
  57. # but don't go above upperTarget
  58. set t [lindex $snapshots $i]
  59. if {$t > $upperTarget} {set t $upperTarget}
  60. reverse goto $t
  61. }
  62. }
  63. proc goto_time_delta {delta} {
  64. set t [expr {[dict get [reverse status] current] + $delta}]
  65. if {$t < 0} {set t 0}
  66. reverse goto $t
  67. }
  68. proc go_back_one_step {} {
  69. goto_time_delta [expr {-$::speed / 100.0}]
  70. }
  71. proc go_forward_one_step {} {
  72. goto_time_delta [expr { $::speed / 100.0}]
  73. }
  74. # reverse bookmarks
  75. variable bookmarks [dict create]
  76. proc create_bookmark_from_current_time {name} {
  77. variable bookmarks
  78. dict set bookmarks $name [machine_info time]
  79. # The next message is useful as part of a hotkey command for this
  80. # osd::display_message "Saved current time to bookmark '$name'"
  81. return "Created bookmark '$name' at [dict get $bookmarks $name]"
  82. }
  83. proc remove_bookmark {name} {
  84. variable bookmarks
  85. dict unset bookmarks $name
  86. return "Removed bookmark '$name'"
  87. }
  88. proc jump_to_bookmark {name} {
  89. variable bookmarks
  90. if {[dict exists $bookmarks $name]} {
  91. reverse goto [dict get $bookmarks $name]
  92. # The next message is useful as part of a hotkey command for
  93. # this
  94. #osd::display_message "Jumped to bookmark '$name'"
  95. } else {
  96. error "Bookmark '$name' not defined..."
  97. }
  98. }
  99. proc clear_bookmarks {} {
  100. variable bookmarks
  101. set bookmarks [dict create]
  102. }
  103. proc save_bookmarks {name} {
  104. variable bookmarks
  105. set directory [file normalize $::env(OPENMSX_USER_DATA)/../reverse_bookmarks]
  106. file mkdir $directory
  107. set fullname [file join $directory ${name}.rbm]
  108. if {[catch {
  109. set the_file [open $fullname {WRONLY TRUNC CREAT}]
  110. puts $the_file $bookmarks
  111. close $the_file
  112. } errorText]} {
  113. error "Failed to save to $fullname: $errorText"
  114. }
  115. return "Successfully saved bookmarks to $fullname"
  116. }
  117. proc load_bookmarks {name} {
  118. variable bookmarks
  119. set directory [file normalize $::env(OPENMSX_USER_DATA)/../reverse_bookmarks]
  120. set fullname [file join $directory ${name}.rbm]
  121. if {[catch {
  122. set the_file [open $fullname {RDONLY}]
  123. set bookmarks [read $the_file]
  124. close $the_file
  125. } errorText]} {
  126. error "Failed to load from $fullname: $errorText"
  127. }
  128. return "Successfully loaded $fullname"
  129. }
  130. proc list_bookmarks_files {} {
  131. set directory [file normalize $::env(OPENMSX_USER_DATA)/../reverse_bookmarks]
  132. set results [list]
  133. foreach f [lsort [glob -tails -directory $directory -type f -nocomplain *.rbm]] {
  134. lappend results [file rootname $f]
  135. }
  136. return $results
  137. }
  138. proc reverse_bookmarks {subcmd args} {
  139. switch -- $subcmd {
  140. "create" {create_bookmark_from_current_time {*}$args}
  141. "remove" {remove_bookmark {*}$args}
  142. "goto" {jump_to_bookmark {*}$args}
  143. "clear" {clear_bookmarks}
  144. "load" {load_bookmarks {*}$args}
  145. "save" {save_bookmarks {*}$args}
  146. default {error "Invalid subcommand: $subcmd"}
  147. }
  148. }
  149. set_help_proc reverse_bookmarks [namespace code reverse_bookmarks_help]
  150. proc reverse_bookmarks_help {args} {
  151. switch -- [lindex $args 1] {
  152. "create" {return {Create a bookmark at the current time with the given name.
  153. Syntax: reverse_bookmarks create <name>
  154. }}
  155. "remove" {return {Remove the bookmark with the given name.
  156. Syntax: reverse_bookmarks remove <name>
  157. }}
  158. "goto" {return {Go to the bookmark with the given name.
  159. Syntax: reverse_bookmarks goto <name>
  160. }}
  161. "clear" {return {Removes all bookmarks.
  162. Syntax: reverse_bookmarks clear
  163. }}
  164. "save" {return {Save the current reverse bookmarks to a file.
  165. Syntax: reverse_bookmarks save <filename>
  166. }}
  167. "load" {return {Load reverse bookmarks from file.
  168. Syntax: reverse_bookmarks load <filename>
  169. }}
  170. default {return {Control the reverse bookmarks functionality.
  171. Syntax: reverse_bookmarks <sub-command> [<arguments>]
  172. Where sub-command is one of:
  173. create Create a bookmark at the current time
  174. remove Remove a previously created bookmark
  175. goto Go to a previously created bookmark
  176. clear Shortcut to remove all bookmarks
  177. save Save current bookmarks to a file
  178. load Load previously saved bookmarks
  179. Use 'help reverse_bookmarks <sub-command>' to get more detailed help on a specific sub-command.
  180. }}
  181. }
  182. }
  183. set_tabcompletion_proc reverse_bookmarks [namespace code reverse_bookmarks_tabcompletion]
  184. proc reverse_bookmarks_tabcompletion {args} {
  185. variable bookmarks
  186. if {[llength $args] == 2} {
  187. return [list "create" "remove" "goto" "clear" "save" "load"]
  188. } elseif {[llength $args] == 3} {
  189. switch -- [lindex $args 1] {
  190. "remove" -
  191. "goto" {return [dict keys $bookmarks]}
  192. "load" -
  193. "save" {return [list_bookmarks_files]}
  194. default {return [list]}
  195. }
  196. }
  197. }
  198. ### auto save replays
  199. trace add variable ::auto_save_replay "write" [namespace code auto_save_setting_changed]
  200. variable old_auto_save_value $::auto_save_replay
  201. variable auto_save_after_id 0
  202. proc auto_save_setting_changed {name1 name2 op} {
  203. variable old_auto_save_value
  204. variable auto_save_after_id
  205. if {$::auto_save_replay != $old_auto_save_value} {
  206. set old_auto_save_value $::auto_save_replay
  207. if {$::auto_save_replay && $auto_save_after_id == 0 } {
  208. set stat_dict [reverse status]
  209. if {[dict get $stat_dict status] eq "disabled"} {
  210. error "Reverse is not enabled!"
  211. }
  212. auto_save_replay_loop
  213. puts "Enabled auto-save of replay to filename $::auto_save_replay_filename every $::auto_save_replay_interval seconds."
  214. } elseif {!$::auto_save_replay && $auto_save_after_id != 0 } {
  215. after cancel $auto_save_after_id
  216. set auto_save_after_id 0
  217. puts "Auto-save of replay disabled."
  218. }
  219. }
  220. }
  221. proc auto_save_replay_loop {} {
  222. variable auto_save_after_id
  223. if {$::auto_save_replay} {
  224. reverse savereplay -maxnofextrasnapshots 0 $::auto_save_replay_filename
  225. set auto_save_after_id [after realtime $::auto_save_replay_interval "reverse::auto_save_replay_loop"]
  226. }
  227. }
  228. namespace export reverse_prev
  229. namespace export reverse_next
  230. namespace export goto_time_delta
  231. namespace export go_back_one_step
  232. namespace export go_forward_one_step
  233. namespace export reverse_bookmarks
  234. } ;# namespace reverse
  235. namespace eval reverse_widgets {
  236. variable reverse_bar_update_interval 0.10
  237. variable update_after_id 0
  238. variable mouse_after_id 0
  239. variable overlay_counter
  240. variable prev_x 0
  241. variable prev_y 0
  242. variable overlayOffset
  243. variable invisibleTime +inf ;# bar is invisble past this time, +inf means it's permanently visible
  244. set_help_text toggle_reversebar \
  245. {Enable/disable an on-screen reverse bar.
  246. This will show the recorded 'reverse history' and the current position in\
  247. this history. It is possible to click on this bar to immediately jump to a\
  248. specific point in time. If the current position is at the end of the history\
  249. (i.e. we're not replaying an existing history), this reverse bar will slowly\
  250. fade out. You can make it reappear by moving the mouse over it.
  251. }
  252. proc toggle_reversebar {} {
  253. if {[osd exists reverse]} {
  254. disable_reversebar
  255. } else {
  256. enable_reversebar
  257. }
  258. return ""
  259. }
  260. proc enable_reversebar {{visible true}} {
  261. reverse::auto_enable
  262. if {[osd exists reverse]} {
  263. # osd already enabled
  264. return
  265. }
  266. # Reversebar
  267. set fade [expr {$visible ? 1.0 : 0.0}]
  268. osd create rectangle reverse \
  269. -scaled true -x 34 -y 1 -w 252 -h 8 \
  270. -rgba 0x00000080 -fadeCurrent $fade -fadeTarget $fade \
  271. -borderrgba 0xFFFFFFC0 -bordersize 1
  272. osd create rectangle reverse.int \
  273. -x 1 -y 1 -w 250 -h 6 -rgba 0x00000000 -clip true
  274. osd create rectangle reverse.int.bar \
  275. -relw 0 -relh 1 -z 3 -rgba "0x0044aa80 0x2266dd80 0x0055cc80 0x55eeff80"
  276. osd create rectangle reverse.int.end \
  277. -relx 0 -x -1 -w 2 -relh 1 -z 3 -rgba 0xff8000c0
  278. osd create text reverse.int.text \
  279. -x -10 -y 0 -relx 0.5 -size 5 -z 6 -rgba 0xffffffff
  280. # on mouse over hover box
  281. osd create rectangle reverse.mousetime \
  282. -relx 0.5 -rely 1 -relh 0.75 -z 4 \
  283. -rgba "0xffdd55e8 0xddbb33e8 0xccaa22e8 0xffdd55e8" \
  284. -bordersize 0.5 -borderrgba 0xffff4480
  285. osd create text reverse.mousetime.text \
  286. -size 5 -z 4 -rgba 0x000000ff
  287. update_reversebar
  288. variable mouse_after_id
  289. set mouse_after_id [after "mouse button1 down" [namespace code check_mouse]]
  290. trace add variable ::reverse::bookmarks "write" [namespace code update_bookmarks]
  291. }
  292. proc disable_reversebar {} {
  293. trace remove variable ::reverse::bookmarks "write" [namespace code update_bookmarks]
  294. variable update_after_id
  295. variable mouse_after_id
  296. after cancel $update_after_id
  297. after cancel $mouse_after_id
  298. osd destroy reverse
  299. }
  300. proc update_reversebar {} {
  301. catch {update_reversebar2}
  302. variable reverse_bar_update_interval
  303. variable update_after_id
  304. set update_after_id [after realtime $reverse_bar_update_interval [namespace code update_reversebar]]
  305. }
  306. proc update_reversebar2 {} {
  307. # Hack: Put the reverse bar at the bottom when the icons are at the top,
  308. # otherwise at the top.
  309. # It would be better to have a proper layout manager for the OSD stuff.
  310. if {[catch {set led_y [osd info osd_icons -y]}]} {
  311. set led_y 0
  312. }
  313. if {[catch {set led_w [osd info osd_icons -w]}]} {
  314. set led_w 0
  315. }
  316. osd configure reverse -y [expr {($led_y < 10) && ($led_w != 0) ? 231 : 1}]
  317. # Set time indicator position (depending on reverse bar position)
  318. variable overlayOffset [expr {($led_y > 16) ? 1.1 : -1.25}]
  319. set stats [reverse status]
  320. set x 2; set y 2
  321. catch {lassign [osd info "reverse.int" -mousecoord] x y}
  322. set mouseInside [expr {0 <= $x && $x <= 1 && 0 <= $y && $y <= 1}]
  323. variable invisibleTime
  324. set now [openmsx_info realtime]
  325. switch [dict get $stats status] {
  326. "disabled" {
  327. disable_reversebar
  328. return
  329. }
  330. "replaying" {
  331. osd configure reverse -fadeTarget 1.0 -fadeCurrent 1.0
  332. if {[reverse viewonlymode]} {
  333. set color "0x00aa44a0 0x22dd66a0 0x00cc55a0 0x55ffeea0"
  334. } else {
  335. set color "0x0044aaa0 0x2266dda0 0x0055cca0 0x55eeffa0"
  336. }
  337. osd configure reverse.int.bar -rgba $color
  338. set invisibleTime +inf
  339. }
  340. "enabled" {
  341. osd configure reverse.int.bar \
  342. -rgba "0xff4400a0 0xdd3300a0 0xbb2200a0 0xcccc11a0"
  343. if {$mouseInside || $::reversebar_fadeout_time == 0.0} {
  344. osd configure reverse -fadePeriod 0.5 -fadeTarget 1.0
  345. set invisibleTime +inf
  346. } else {
  347. osd configure reverse -fadePeriod $::reversebar_fadeout_time -fadeTarget 0.0
  348. if {$invisibleTime == +inf} {
  349. set invisibleTime [expr {$now + $::reversebar_fadeout_time}]
  350. }
  351. }
  352. }
  353. }
  354. if {$now > $invisibleTime} {
  355. # Optimization: reverse bar is completely faded out,
  356. # don't bother keeping it up to date.
  357. return
  358. }
  359. set snapshots [dict get $stats snapshots]
  360. set begin [dict get $stats begin]
  361. set end [dict get $stats end]
  362. set current [dict get $stats current]
  363. set totLenght [expr {$end - $begin}]
  364. set playLength [expr {$current - $begin}]
  365. set reciprocalLength [expr {($totLenght != 0) ? (1.0 / $totLenght) : 0}]
  366. set fraction [expr {$playLength * $reciprocalLength}]
  367. # Check if cursor moved compared to previous update,
  368. # if so reset counter (see below)
  369. variable overlay_counter
  370. variable prev_x
  371. variable prev_y
  372. if {$prev_x != $x || $prev_y != $y} {
  373. set overlay_counter 0
  374. set prev_x $x
  375. set prev_y $y
  376. }
  377. # Display mouse-over time jump-indicator
  378. # Hide when mouse hasn't moved for some time
  379. if {$mouseInside && $overlay_counter < 8} {
  380. variable overlayOffset
  381. set mousetext [utils::format_time_subseconds [expr {$x * $totLenght}]]
  382. osd configure reverse.mousetime.text -text $mousetext -relx 0.05
  383. set textsize [lindex [osd info reverse.mousetime.text -query-size] 0]
  384. osd configure reverse.mousetime -rely $overlayOffset -relx [expr {$x - 0.05}] -w [expr {1.1 * $textsize}]
  385. incr overlay_counter
  386. } else {
  387. osd configure reverse.mousetime -rely -100
  388. }
  389. # snapshots
  390. set count 0
  391. foreach snapshot $snapshots {
  392. set name reverse.int.tick$count
  393. if {![osd exists $name]} {
  394. # create new if it doesn't exist yet
  395. osd create rectangle $name -w 0.5 -relh 1 -rgba 0x444444ff -z 2
  396. }
  397. osd configure $name -relx [expr {($snapshot - $begin) * $reciprocalLength}]
  398. incr count
  399. }
  400. # destroy all snapshots with higher count number
  401. while {[osd destroy reverse.int.tick$count]} {
  402. incr count
  403. }
  404. # bookmarks
  405. set count 0
  406. dict for {bookmarkname bookmarkval} $::reverse::bookmarks {
  407. set name reverse.bookmark$count
  408. if {![osd exists $name]} {
  409. # create new if it doesn't exist yet
  410. osd create rectangle $name \
  411. -relx 0.5 -rely 1 -relh 0.75 -z 3 \
  412. -rgba "0xffdd55e8 0xddbb33e8 0xccaa22e8 0xffdd55e8" \
  413. -bordersize 0.5 -borderrgba 0xffff4480
  414. osd create text $name.text -relx -0.05 \
  415. -size 5 -z 3 -rgba 0x000000ff -text $bookmarkname
  416. set textsize [lindex [osd info $name.text -query-size] 0]
  417. osd configure $name -w [expr {1.1 * $textsize}]
  418. }
  419. osd configure $name -relx [expr {($bookmarkval - $begin) * $reciprocalLength}]
  420. incr count
  421. }
  422. # Round fraction to avoid excessive redraws caused by rounding errors
  423. set fraction [expr {round($fraction * 10000) / 10000.0}]
  424. osd configure reverse.int.bar -relw $fraction
  425. osd configure reverse.int.end -relx $fraction
  426. osd configure reverse.int.text \
  427. -text "[utils::format_time_subseconds $playLength] / [utils::format_time_subseconds $totLenght]"
  428. }
  429. proc check_mouse {} {
  430. catch {
  431. # click on reverse bar
  432. set x 2; set y 2
  433. catch {lassign [osd info "reverse.int" -mousecoord] x y}
  434. if {0 <= $x && $x <= 1 && 0 <= $y && $y <= 1} {
  435. set stats [reverse status]
  436. set begin [dict get $stats begin]
  437. set end [dict get $stats end]
  438. reverse goto [expr {$begin + $x * ($end - $begin)}]
  439. }
  440. # click on bookmark
  441. set count 0
  442. dict for {bookmarkname bookmarkval} $::reverse::bookmarks {
  443. set name reverse.bookmark$count
  444. set x 2; set y 2
  445. catch {lassign [osd info $name -mousecoord] x y}
  446. if {0 <= $x && $x <= 1 && 0 <= $y && $y <= 1} {
  447. reverse::jump_to_bookmark $bookmarkname
  448. }
  449. incr count
  450. }
  451. }
  452. variable mouse_after_id
  453. set mouse_after_id [after "mouse button1 down" [namespace code check_mouse]]
  454. }
  455. proc update_bookmarks {name1 name2 op} {
  456. # remove all bookmarks to make sure the widget names start with 0
  457. # and have no 'holes' in their sequence. They will be recreated
  458. # when necessary in update_reversebar2
  459. set count 0
  460. while {[osd destroy reverse.bookmark$count]} {
  461. incr count
  462. }
  463. update_reversebar
  464. }
  465. namespace export toggle_reversebar
  466. namespace export enable_reversebar
  467. namespace export disable_reversebar
  468. } ;# namespace reverse_widgets
  469. namespace import reverse::*
  470. namespace import reverse_widgets::*