badline 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/CLAUDE.md +3 -1
- data/README.md +96 -290
- data/doc/library.md +62 -0
- data/doc/media.md +69 -0
- data/doc/sid-player.md +132 -0
- data/doc/snapshots.md +69 -0
- data/lib/badline/cartridge/freezer.rb +2 -0
- data/lib/badline/cartridge.rb +3 -0
- data/lib/badline/frontend/app.rb +38 -35
- data/lib/badline/frontend/boot.rb +8 -1
- data/lib/badline/frontend/buttons.rb +101 -7
- data/lib/badline/frontend/confirmation.rb +50 -0
- data/lib/badline/frontend/controls.rb +44 -32
- data/lib/badline/frontend/file_browser.rb +225 -0
- data/lib/badline/frontend/frame_report.rb +52 -0
- data/lib/badline/frontend/menu_dialogs.rb +122 -0
- data/lib/badline/frontend/menu_media.rb +128 -0
- data/lib/badline/frontend/menu_pages.rb +198 -0
- data/lib/badline/frontend/painter.rb +10 -0
- data/lib/badline/frontend/pause_menu.rb +246 -0
- data/lib/badline/frontend/snapshots.rb +1 -1
- data/lib/badline/frontend/sound.rb +0 -1
- data/lib/badline/frontend/timeline.rb +5 -1
- data/lib/badline/frontend.rb +7 -0
- data/lib/badline/media/disk_set.rb +108 -0
- data/lib/badline/media.rb +1 -0
- data/lib/badline/options/table.rb +1 -1
- data/lib/badline/options.rb +3 -3
- data/lib/badline/version.rb +1 -1
- metadata +13 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The pause menu's question before something power cycles the machine:
|
|
6
|
+
# a cartridge going in or out, or Quick open starting a file. CANCEL
|
|
7
|
+
# has the focus, so Return alone changes nothing.
|
|
8
|
+
class Confirmation
|
|
9
|
+
TITLES = { cartridge: "INSERT A CARTRIDGE", remove: "REMOVE THE CARTRIDGE", start: "QUICK OPEN" }.freeze
|
|
10
|
+
WARNINGS = { cartridge: "INSERTING IT POWER CYCLES THE MACHINE.", remove: "REMOVING IT POWER CYCLES THE MACHINE.",
|
|
11
|
+
start: "THIS STARTS A NEW MACHINE." }.freeze
|
|
12
|
+
ANSWERS = { cartridge: "INSERT", remove: "REMOVE", start: "START" }.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(painter, buttons)
|
|
15
|
+
@painter = painter
|
|
16
|
+
@buttons = buttons
|
|
17
|
+
@kind = :start
|
|
18
|
+
@path = ""
|
|
19
|
+
@open = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def open? = @open
|
|
23
|
+
|
|
24
|
+
# Asks about `kind`, :cartridge, :remove or :start, for the file at
|
|
25
|
+
# `path`.
|
|
26
|
+
def ask(kind, path)
|
|
27
|
+
@kind = kind
|
|
28
|
+
@path = path
|
|
29
|
+
@open = true
|
|
30
|
+
@buttons.focus = :cancel
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Puts the question away, and returns its kind and path.
|
|
34
|
+
def close
|
|
35
|
+
@open = false
|
|
36
|
+
[@kind, @path]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def draw(left, top)
|
|
40
|
+
painter = @painter
|
|
41
|
+
painter.text(left, top, TITLES.fetch(@kind), PauseMenu::BRIGHT)
|
|
42
|
+
painter.text(left, top + 20, Painter.fit(File.basename(@path), 40), PauseMenu::TEXT) unless @path.empty?
|
|
43
|
+
painter.text(left, top + 44, WARNINGS.fetch(@kind), PauseMenu::TEXT)
|
|
44
|
+
painter.text(left, top + 56, "ANYTHING NOT SAVED IS LOST.", PauseMenu::TEXT)
|
|
45
|
+
@buttons.row([left, top + 80, MenuPages::ROW_WIDTH], "CANCEL", :cancel)
|
|
46
|
+
@buttons.row([left, top + 80 + Buttons::HEIGHT + 2, MenuPages::ROW_WIDTH], ANSWERS.fetch(@kind), :confirm)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -2,53 +2,72 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# Routes host input to the machine
|
|
6
|
-
# steps through and shift-Tab steps back through:
|
|
5
|
+
# Routes host input to the machine. Two settings decide where it goes:
|
|
7
6
|
#
|
|
8
|
-
# -
|
|
9
|
-
#
|
|
10
|
-
# and left shift the other. The arrows start on joystick 2, and
|
|
11
|
-
# swaps them.
|
|
12
|
-
# -
|
|
13
|
-
#
|
|
14
|
-
# paddles or moves the mouse, and the host's left and right
|
|
15
|
-
# to whichever lines the device puts them on.
|
|
7
|
+
# - The keys: every host key goes to the C64 keyboard, or, as Tab
|
|
8
|
+
# switches, the arrow cluster and space drive one joystick and WASD
|
|
9
|
+
# and left shift the other. The arrows start on joystick 2, and
|
|
10
|
+
# #swap_ports swaps them.
|
|
11
|
+
# - A pot device the pause menu plugs into a control port: a 1351
|
|
12
|
+
# mouse or a pair of paddles, which the host mouse drives. Motion
|
|
13
|
+
# turns the paddles or moves the mouse, and the host's left and right
|
|
14
|
+
# buttons go to whichever lines the device puts them on.
|
|
16
15
|
class Controls
|
|
17
|
-
|
|
18
|
-
TAGS = ["", "
|
|
16
|
+
POTS = %i[none mouse1 mouse2 paddles1 paddles2].freeze
|
|
17
|
+
TAGS = ["", "MOUSE 1", "MOUSE 2", "PADDLE 1", "PADDLE 2"].freeze
|
|
19
18
|
|
|
20
19
|
# SDL mouse button numbers.
|
|
21
20
|
MOUSE_BUTTONS = { 1 => :left, 3 => :right }.freeze
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
# The pot device plugged in, one of POTS.
|
|
23
|
+
attr_reader :pot, :arrows_port
|
|
24
24
|
|
|
25
25
|
def initialize(computer)
|
|
26
26
|
@computer = computer
|
|
27
|
-
@
|
|
27
|
+
@joystick = false
|
|
28
|
+
@pot = :none
|
|
28
29
|
@arrows_port = 2
|
|
29
30
|
@mouse = Input::Mouse1351.new
|
|
30
31
|
@paddles = Input::Paddles.new
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
# Routes the input to another machine, the
|
|
34
|
-
#
|
|
34
|
+
# Routes the input to another machine, the pot device going into its
|
|
35
|
+
# port.
|
|
35
36
|
def computer=(computer)
|
|
36
37
|
release_all
|
|
37
38
|
@computer = computer
|
|
38
39
|
attach_pot_device
|
|
39
40
|
end
|
|
40
41
|
|
|
41
|
-
def joystick_mode? = @
|
|
42
|
+
def joystick_mode? = @joystick
|
|
43
|
+
|
|
44
|
+
# Sends the keys to the joysticks, or with false to the keyboard.
|
|
45
|
+
def joystick_mode=(joystick)
|
|
46
|
+
@joystick = joystick
|
|
47
|
+
release_all
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def toggle_keys
|
|
51
|
+
self.joystick_mode = !@joystick
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Plugs in one of POTS, in place of the one before.
|
|
55
|
+
def plug(pot)
|
|
56
|
+
@pot = pot
|
|
57
|
+
release_all
|
|
58
|
+
attach_pot_device
|
|
59
|
+
end
|
|
42
60
|
|
|
43
61
|
# Whether the host mouse drives a pot device, and so should be held
|
|
44
62
|
# in relative mode.
|
|
45
|
-
def pot_device? =
|
|
63
|
+
def pot_device? = @pot != :none
|
|
46
64
|
|
|
47
|
-
# The title bar's tag for the
|
|
65
|
+
# The title bar's tag for the settings, empty for the keyboard alone.
|
|
48
66
|
def tag
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
TAGS[
|
|
67
|
+
tags = []
|
|
68
|
+
tags << "JOY #{@arrows_port}" if @joystick
|
|
69
|
+
tags << TAGS[POTS.index(@pot)] if pot_device?
|
|
70
|
+
tags.join(", ")
|
|
52
71
|
end
|
|
53
72
|
|
|
54
73
|
def key(scancode, down)
|
|
@@ -59,13 +78,6 @@ module Badline
|
|
|
59
78
|
end
|
|
60
79
|
end
|
|
61
80
|
|
|
62
|
-
# Steps to the next mode, or back with a negative step.
|
|
63
|
-
def cycle_mode(step)
|
|
64
|
-
@mode = MODES[(MODES.index(@mode) + step) % MODES.size]
|
|
65
|
-
release_all
|
|
66
|
-
attach_pot_device
|
|
67
|
-
end
|
|
68
|
-
|
|
69
81
|
def swap_ports
|
|
70
82
|
@arrows_port = 3 - @arrows_port
|
|
71
83
|
release_all
|
|
@@ -99,9 +111,9 @@ module Badline
|
|
|
99
111
|
end
|
|
100
112
|
end
|
|
101
113
|
|
|
102
|
-
def paddles? =
|
|
114
|
+
def paddles? = POTS.index(@pot) >= 3
|
|
103
115
|
|
|
104
|
-
# A fresh device on
|
|
116
|
+
# A fresh pot device on its port, and none on the other.
|
|
105
117
|
def attach_pot_device
|
|
106
118
|
ports = @computer.control_ports
|
|
107
119
|
ports.device1 = nil
|
|
@@ -133,7 +145,7 @@ module Badline
|
|
|
133
145
|
end
|
|
134
146
|
end
|
|
135
147
|
|
|
136
|
-
def port_one? = %i[mouse1 paddles1].include?(@
|
|
148
|
+
def port_one? = %i[mouse1 paddles1].include?(@pot)
|
|
137
149
|
|
|
138
150
|
def joystick_key(scancode, down)
|
|
139
151
|
direction = Keys.arrows(scancode)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The pause menu's file browser, which lists a folder's subfolders and
|
|
6
|
+
# the files a device takes, folders first, each by name. The keys move
|
|
7
|
+
# through the list, open a folder or pick a file, go up a folder, and
|
|
8
|
+
# jump to the next name starting with a letter or digit. A click picks
|
|
9
|
+
# the row under it, and the wheel scrolls.
|
|
10
|
+
class FileBrowser
|
|
11
|
+
ROW = 12
|
|
12
|
+
ROWS = 12
|
|
13
|
+
COLUMNS = 40
|
|
14
|
+
|
|
15
|
+
RETURN = 40
|
|
16
|
+
ESCAPE = 41
|
|
17
|
+
BACKSPACE = 42
|
|
18
|
+
HOME = 74
|
|
19
|
+
PAGE_UP = 75
|
|
20
|
+
END_KEY = 77
|
|
21
|
+
PAGE_DOWN = 78
|
|
22
|
+
RIGHT = 79
|
|
23
|
+
LEFT = 80
|
|
24
|
+
DOWN = 81
|
|
25
|
+
UP = 82
|
|
26
|
+
DIGITS = "1234567890"
|
|
27
|
+
|
|
28
|
+
attr_reader :directory
|
|
29
|
+
|
|
30
|
+
def initialize(painter, buttons)
|
|
31
|
+
@painter = painter
|
|
32
|
+
@buttons = buttons
|
|
33
|
+
@open = false
|
|
34
|
+
@directory = Dir.pwd
|
|
35
|
+
@extensions = []
|
|
36
|
+
@names = []
|
|
37
|
+
@folders = 0
|
|
38
|
+
@selected = 0
|
|
39
|
+
@top = 0
|
|
40
|
+
@list_top = 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def open? = @open
|
|
44
|
+
|
|
45
|
+
# What a name sorts by: lower case, with its numbers padded, so disk 2
|
|
46
|
+
# comes before disk 10.
|
|
47
|
+
def self.order(name) = name.downcase.gsub(/\d+/) { |digits| digits.rjust(10, "0") }
|
|
48
|
+
|
|
49
|
+
# Lists `directory` for the files with one of `extensions`.
|
|
50
|
+
def open(directory, extensions)
|
|
51
|
+
@open = true
|
|
52
|
+
@extensions = extensions
|
|
53
|
+
enter(directory)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def close
|
|
57
|
+
@open = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Handles a key, and returns the path of a file picked, :cancel when
|
|
61
|
+
# Esc leaves the browser, or nil.
|
|
62
|
+
def key(scancode)
|
|
63
|
+
return :cancel if scancode == ESCAPE
|
|
64
|
+
return pick if [RETURN, RIGHT].include?(scancode)
|
|
65
|
+
return up if [LEFT, BACKSPACE].include?(scancode)
|
|
66
|
+
|
|
67
|
+
select(@selected + step(scancode)) unless step(scancode).zero?
|
|
68
|
+
jump(scancode)
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Picks the row at `top`, as Return would.
|
|
73
|
+
def click(top)
|
|
74
|
+
row = (top - @list_top) / ROW
|
|
75
|
+
return nil if row.negative? || @top + row >= @names.size
|
|
76
|
+
|
|
77
|
+
@selected = @top + row
|
|
78
|
+
pick
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def scroll(rows)
|
|
82
|
+
@top = (@top + rows).clamp(0, [@names.size - ROWS, 0].max)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def draw(left, top, width, title)
|
|
86
|
+
@painter.text(left, top, title, PauseMenu::BRIGHT)
|
|
87
|
+
@painter.text(left, top + ROW + 2, shown_directory, PauseMenu::DIM)
|
|
88
|
+
@list_top = top + (ROW * 2) + 8
|
|
89
|
+
@buttons.area([left, @list_top, width, ROW * ROWS], :browse)
|
|
90
|
+
draw_rows(left, width)
|
|
91
|
+
draw_bar(left + width - 2) if @names.size > ROWS
|
|
92
|
+
@painter.text(left, @list_top + (ROW * ROWS) + 6, "RETURN: OPEN LEFT: BACK ESC: CANCEL",
|
|
93
|
+
PauseMenu::DIM)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# The folder's path, cut at the start to fit.
|
|
99
|
+
def shown_directory
|
|
100
|
+
length = @directory.length
|
|
101
|
+
length > COLUMNS ? "...#{@directory[length - COLUMNS + 3, COLUMNS - 3]}" : @directory
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def draw_rows(left, width)
|
|
105
|
+
ROWS.times do |row|
|
|
106
|
+
index = @top + row
|
|
107
|
+
break if index >= @names.size
|
|
108
|
+
|
|
109
|
+
name = label(index)
|
|
110
|
+
top = @list_top + (row * ROW)
|
|
111
|
+
if index == @selected
|
|
112
|
+
@painter.box(left - 2, top - 2, width - 4, ROW, PauseMenu::TEXT)
|
|
113
|
+
@painter.text(left, top, name, PauseMenu::PANEL)
|
|
114
|
+
else
|
|
115
|
+
@painter.text(left, top, name, index < @folders ? PauseMenu::BRIGHT : PauseMenu::TEXT)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def draw_bar(left)
|
|
121
|
+
height = ROW * ROWS
|
|
122
|
+
size = [(height * ROWS) / @names.size, 4].max
|
|
123
|
+
offset = ((height - size) * @top) / [@names.size - ROWS, 1].max
|
|
124
|
+
@painter.box(left, @list_top - 2, 2, height, PauseMenu::EDGE)
|
|
125
|
+
@painter.box(left, @list_top - 2 + offset, 2, size, PauseMenu::TEXT)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def label(index)
|
|
129
|
+
name = @names[index]
|
|
130
|
+
name += "/" if index < @folders
|
|
131
|
+
Painter.fit(name, COLUMNS)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def step(scancode)
|
|
135
|
+
case scancode
|
|
136
|
+
when UP then -1
|
|
137
|
+
when DOWN then 1
|
|
138
|
+
when PAGE_UP then -ROWS
|
|
139
|
+
when PAGE_DOWN then ROWS
|
|
140
|
+
when HOME then -@names.size
|
|
141
|
+
when END_KEY then @names.size
|
|
142
|
+
else 0
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def select(index)
|
|
147
|
+
return if @names.empty?
|
|
148
|
+
|
|
149
|
+
@selected = index.clamp(0, @names.size - 1)
|
|
150
|
+
@top = @selected if @selected < @top
|
|
151
|
+
@top = @selected - ROWS + 1 if @selected >= @top + ROWS
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Selects the next name after the one selected that starts with the
|
|
155
|
+
# key's letter or digit.
|
|
156
|
+
def jump(scancode)
|
|
157
|
+
char = if scancode.between?(4, 29) then (97 + scancode - 4).chr
|
|
158
|
+
elsif scancode.between?(30, 39) then DIGITS[scancode - 30]
|
|
159
|
+
else ""
|
|
160
|
+
end
|
|
161
|
+
return if char.empty? || @names.empty?
|
|
162
|
+
|
|
163
|
+
@names.size.times do |offset|
|
|
164
|
+
index = (@selected + 1 + offset) % @names.size
|
|
165
|
+
next unless @names[index].downcase.start_with?(char)
|
|
166
|
+
|
|
167
|
+
select(index)
|
|
168
|
+
break
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def pick
|
|
173
|
+
return nil if @names.empty?
|
|
174
|
+
|
|
175
|
+
path = File.join(@directory, @names[@selected])
|
|
176
|
+
return path unless @selected < @folders
|
|
177
|
+
|
|
178
|
+
@names[@selected] == ".." ? up : enter(path)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def up
|
|
182
|
+
parent = File.dirname(@directory)
|
|
183
|
+
return nil if parent == @directory
|
|
184
|
+
|
|
185
|
+
from = File.basename(@directory)
|
|
186
|
+
enter(parent)
|
|
187
|
+
index = @names.index(from)
|
|
188
|
+
select(index) unless index.nil?
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def enter(directory)
|
|
193
|
+
@directory = File.expand_path(directory)
|
|
194
|
+
folders = []
|
|
195
|
+
files = []
|
|
196
|
+
children(@directory).each do |name|
|
|
197
|
+
next if name.start_with?(".")
|
|
198
|
+
|
|
199
|
+
if File.directory?(File.join(@directory, name))
|
|
200
|
+
folders << name
|
|
201
|
+
elsif @extensions.include?(File.extname(name).downcase)
|
|
202
|
+
files << name
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
listing(folders, files)
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def listing(folders, files)
|
|
210
|
+
folders = folders.sort_by { |name| FileBrowser.order(name) }
|
|
211
|
+
folders.unshift("..") unless File.dirname(@directory) == @directory
|
|
212
|
+
@folders = folders.size
|
|
213
|
+
@names = folders + files.sort_by { |name| FileBrowser.order(name) }
|
|
214
|
+
@selected = 0
|
|
215
|
+
@top = 0
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def children(directory)
|
|
219
|
+
Dir.children(directory)
|
|
220
|
+
rescue SystemCallError
|
|
221
|
+
[]
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The --verbose report every 50 frames: the frame rate, the time each
|
|
6
|
+
# stage of a frame took on average, the slowest frame's work, and how
|
|
7
|
+
# the sound's queue fared.
|
|
8
|
+
class FrameReport
|
|
9
|
+
STAGES = %w[events emulate audio blit present wait].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(sound)
|
|
12
|
+
@sound = sound
|
|
13
|
+
@reported_samples = 0
|
|
14
|
+
clear
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Adds a frame's stamps, one as each stage starts and one after the
|
|
18
|
+
# last.
|
|
19
|
+
def add(stamps)
|
|
20
|
+
STAGES.size.times { |stage| @spent[stage] += stamps[stage + 1] - stamps[stage] }
|
|
21
|
+
took = stamps[-2] - stamps.first
|
|
22
|
+
@slowest = took if took > @slowest
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Reports the 50 frames added over the last `seconds`, and starts
|
|
26
|
+
# over.
|
|
27
|
+
def show(seconds)
|
|
28
|
+
stages = STAGES.each_with_index.map { |name, stage| "#{name} #{(@spent[stage] * 20).round(2)}" }
|
|
29
|
+
puts "#{(50 / seconds).round(1)} fps, per frame ms: #{stages.join(' ')}, " \
|
|
30
|
+
"slowest work #{(@slowest * 1000).round(2)}"
|
|
31
|
+
show_sound(seconds) if @sound.on?
|
|
32
|
+
clear
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def clear
|
|
38
|
+
@spent = Array.new(STAGES.size, 0.0)
|
|
39
|
+
@slowest = 0.0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def show_sound(seconds)
|
|
43
|
+
sound = @sound
|
|
44
|
+
rate = (sound.queued - @reported_samples) / seconds
|
|
45
|
+
queue = sound.high.zero? ? "empty" : "#{(sound.low * 1000).round(1)}-#{(sound.high * 1000).round(1)} ms"
|
|
46
|
+
puts " sound #{rate.round} samples/s, queue #{queue}, #{sound.underruns} underruns, #{sound.dropped} dropped"
|
|
47
|
+
sound.reset_levels
|
|
48
|
+
@reported_samples = sound.queued
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The pause menu's screens that take over its panel: the file browser
|
|
6
|
+
# INSERT and Quick open bring up, and the question before a cartridge
|
|
7
|
+
# or Quick open power cycles the machine. A disk or tape picked goes
|
|
8
|
+
# straight in.
|
|
9
|
+
class MenuDialogs
|
|
10
|
+
EXTENSIONS = {
|
|
11
|
+
disk: PauseMenu::DISKS, tape: %w[.tap], cartridge: %w[.crt],
|
|
12
|
+
program: %w[.d64 .d71 .d81 .g64 .t64 .tap .crt .prg .p00 .sid]
|
|
13
|
+
}.freeze
|
|
14
|
+
TITLES = { disk: "INSERT A DISK", tape: "INSERT A TAPE", cartridge: "INSERT A CARTRIDGE",
|
|
15
|
+
program: "QUICK OPEN" }.freeze
|
|
16
|
+
|
|
17
|
+
ESCAPE = 41
|
|
18
|
+
RETURN = 40
|
|
19
|
+
SPACE = 44
|
|
20
|
+
|
|
21
|
+
# The machine Quick open started.
|
|
22
|
+
attr_reader :computer
|
|
23
|
+
|
|
24
|
+
def initialize(painter, buttons, media, options)
|
|
25
|
+
media_path = options.media_path.to_s
|
|
26
|
+
@buttons = buttons
|
|
27
|
+
@media = media
|
|
28
|
+
@options = options
|
|
29
|
+
@browser = FileBrowser.new(painter, buttons)
|
|
30
|
+
@confirmation = Confirmation.new(painter, buttons)
|
|
31
|
+
@browsing = nil
|
|
32
|
+
@browse_from = media_path.empty? ? Dir.pwd : File.dirname(File.expand_path(media_path))
|
|
33
|
+
@computer = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def open? = !@browsing.nil? || @confirmation.open?
|
|
37
|
+
|
|
38
|
+
# Opens the browser on the files for `kind`: :disk, :tape, :cartridge
|
|
39
|
+
# or :program.
|
|
40
|
+
def browse(kind)
|
|
41
|
+
@browsing = kind
|
|
42
|
+
@browser.open(@browse_from, EXTENSIONS.fetch(kind))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ask_remove = @confirmation.ask(:remove, @media.cartridge_name)
|
|
46
|
+
|
|
47
|
+
# Puts a dropped disk or tape in its device, or asks before a
|
|
48
|
+
# cartridge or a program starts, and says whether it asks. Other
|
|
49
|
+
# files are left alone.
|
|
50
|
+
def drop(path)
|
|
51
|
+
extension = File.extname(path).downcase
|
|
52
|
+
return @media.insert(:disk, path) && false if EXTENSIONS[:disk].include?(extension)
|
|
53
|
+
return @media.insert(:tape, path) && false if EXTENSIONS[:tape].include?(extension)
|
|
54
|
+
return false unless EXTENSIONS[:program].include?(extension)
|
|
55
|
+
|
|
56
|
+
@confirmation.ask(extension == ".crt" ? :cartridge : :start, path)
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Handles a key, and returns :resume or :swap once a question is
|
|
61
|
+
# answered yes, or nil.
|
|
62
|
+
def key(scancode)
|
|
63
|
+
return browsed(@browser.key(scancode)) unless @browsing.nil?
|
|
64
|
+
return answer(:cancel) if scancode == ESCAPE
|
|
65
|
+
return answer(@buttons.focus) if [RETURN, SPACE].include?(scancode)
|
|
66
|
+
|
|
67
|
+
direction = PauseMenu::ARROWS[scancode]
|
|
68
|
+
@buttons.shift(direction == :up ? -1 : 1, 1) if %i[up down].include?(direction)
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Handles a click on the browser's list at `top`, or on a question's
|
|
73
|
+
# answer, `action`.
|
|
74
|
+
def click(action, top) = action == :browse ? browsed(@browser.click(top)) : answer(action)
|
|
75
|
+
|
|
76
|
+
def scroll(rows) = @browser.scroll(rows)
|
|
77
|
+
|
|
78
|
+
def draw(left, top, width)
|
|
79
|
+
return @browser.draw(left, top, width, TITLES.fetch(@browsing)) unless @browsing.nil?
|
|
80
|
+
|
|
81
|
+
@confirmation.draw(left, top)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Puts the disk or tape picked in, or asks before a cartridge or
|
|
87
|
+
# Quick open power cycles the machine, and leaves the browser, as Esc
|
|
88
|
+
# does.
|
|
89
|
+
def browsed(picked)
|
|
90
|
+
return nil if picked.nil?
|
|
91
|
+
|
|
92
|
+
kind = @browsing
|
|
93
|
+
@browse_from = @browser.directory
|
|
94
|
+
@browsing = nil
|
|
95
|
+
return nil if picked == :cancel
|
|
96
|
+
|
|
97
|
+
if %i[disk tape].include?(kind)
|
|
98
|
+
@media.insert(kind, picked)
|
|
99
|
+
else
|
|
100
|
+
@confirmation.ask(kind == :cartridge ? :cartridge : :start, picked)
|
|
101
|
+
end
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Does what the question asked about if `action` is :confirm, and
|
|
106
|
+
# returns :resume, or :swap for a new machine.
|
|
107
|
+
def answer(action)
|
|
108
|
+
question = @confirmation.close
|
|
109
|
+
return nil unless action == :confirm
|
|
110
|
+
|
|
111
|
+
path = question[1]
|
|
112
|
+
case question[0]
|
|
113
|
+
when :cartridge then @media.attach_cartridge(path) ? :resume : nil
|
|
114
|
+
when :remove then @media.remove_cartridge && :resume
|
|
115
|
+
else
|
|
116
|
+
@computer = @media.start(@options, path)
|
|
117
|
+
@computer.nil? ? nil : :swap
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|