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,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# What the pause menu puts in the machine and takes out: disks, tapes
|
|
6
|
+
# and cartridges, whether disks are writable, which carries over from
|
|
7
|
+
# one disk to the next, and the disk's set, which PREVIOUS and NEXT step
|
|
8
|
+
# through. It keeps what last went wrong for the page to show.
|
|
9
|
+
class MenuMedia
|
|
10
|
+
attr_reader :disk_path, :problem
|
|
11
|
+
attr_writer :computer
|
|
12
|
+
|
|
13
|
+
def initialize(disk_path, writable)
|
|
14
|
+
@disk_path = disk_path
|
|
15
|
+
@writable = writable
|
|
16
|
+
@set_path = ""
|
|
17
|
+
@set = []
|
|
18
|
+
@problem = ""
|
|
19
|
+
@computer = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def writable? = @writable
|
|
23
|
+
|
|
24
|
+
def inserted?
|
|
25
|
+
drive = @computer.drive1541
|
|
26
|
+
drive.nil? ? @computer.mounted? : !drive.disk.nil?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Puts a disk in device 8 or a tape in the datasette, `kind` :disk or
|
|
30
|
+
# :tape.
|
|
31
|
+
def insert(kind, path)
|
|
32
|
+
@problem = ""
|
|
33
|
+
if kind == :disk
|
|
34
|
+
Media.insert_disk(@computer, path, read_only: !@writable)
|
|
35
|
+
@disk_path = path
|
|
36
|
+
else
|
|
37
|
+
@computer.datasette.insert(Storage::TAP.new(path))
|
|
38
|
+
end
|
|
39
|
+
rescue ArgumentError, SystemCallError, Storage::TAP::FormatError, Storage::T64::FormatError,
|
|
40
|
+
Storage::G64Image::FormatError => e
|
|
41
|
+
@problem = e.message
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Does what the Drive page's :eject_disk, :previous_disk, :next_disk,
|
|
45
|
+
# :protect or :unprotect asks.
|
|
46
|
+
def drive(action)
|
|
47
|
+
case action
|
|
48
|
+
when :eject_disk then eject_disk
|
|
49
|
+
when :previous_disk then step_disk(-1)
|
|
50
|
+
when :next_disk then step_disk(1)
|
|
51
|
+
else writable(action == :unprotect)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def eject_disk
|
|
56
|
+
drive = @computer.drive1541
|
|
57
|
+
drive.nil? ? @computer.unmount : drive.insert(nil)
|
|
58
|
+
@disk_path = ""
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Sets whether disks are writable, for the disk in the drive too,
|
|
62
|
+
# which goes in again as the notch now says.
|
|
63
|
+
def writable(writable)
|
|
64
|
+
@writable = writable
|
|
65
|
+
insert(:disk, @disk_path) if inserted? && !@disk_path.empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The disks of the set the disk in the drive belongs to.
|
|
69
|
+
def disk_set
|
|
70
|
+
return [] if @disk_path.empty?
|
|
71
|
+
|
|
72
|
+
unless @set_path == @disk_path
|
|
73
|
+
@set = Media::DiskSet.around(@disk_path)
|
|
74
|
+
@set_path = @disk_path
|
|
75
|
+
end
|
|
76
|
+
@set
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Puts the disk `step` before or after this one in its set in the
|
|
80
|
+
# drive, if there is one.
|
|
81
|
+
def step_disk(step)
|
|
82
|
+
set = disk_set
|
|
83
|
+
index = set.index(@disk_path)
|
|
84
|
+
return if index.nil? || !(index + step).between?(0, set.size - 1)
|
|
85
|
+
|
|
86
|
+
insert(:disk, set[index + step])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Swaps the cartridge for the one at `path`, which power cycles the
|
|
90
|
+
# machine, and says whether it went in.
|
|
91
|
+
def attach_cartridge(path)
|
|
92
|
+
@problem = ""
|
|
93
|
+
Media.attach(@computer, path)
|
|
94
|
+
true
|
|
95
|
+
rescue ArgumentError, SystemCallError, Storage::CRTFile::FormatError, Cartridge::UnsupportedTypeError => e
|
|
96
|
+
@problem = e.message
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The name the cartridge in the expansion port gives itself, or an
|
|
101
|
+
# empty one.
|
|
102
|
+
def cartridge_name
|
|
103
|
+
cartridge = @computer.address_bus.cartridge
|
|
104
|
+
cartridge.nil? ? "" : cartridge.name
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Takes the cartridge out with the power off.
|
|
108
|
+
def remove_cartridge
|
|
109
|
+
@computer.address_bus.detach_cartridge
|
|
110
|
+
@computer.power_cycle!
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# A new machine started on `path`, as the command line would, or nil
|
|
114
|
+
# when it won't start.
|
|
115
|
+
def start(options, path)
|
|
116
|
+
@problem = ""
|
|
117
|
+
computer = Frontend.start(options, path, @writable)
|
|
118
|
+
@disk_path = PauseMenu.disk_path(path)
|
|
119
|
+
computer
|
|
120
|
+
rescue ArgumentError, SystemCallError, Media::TrueDrive::Error, Storage::SIDFile::FormatError,
|
|
121
|
+
Storage::T64::FormatError, Storage::TAP::FormatError, Storage::CRTFile::FormatError,
|
|
122
|
+
Storage::G64Image::FormatError, Cartridge::UnsupportedTypeError => e
|
|
123
|
+
@problem = e.message
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The pause menu's sections, each showing a part of the machine's
|
|
6
|
+
# hardware as it stands, in one column: what's there, and under it a
|
|
7
|
+
# row for each thing to do, a setting showing its value on the right,
|
|
8
|
+
# which a press steps on.
|
|
9
|
+
class MenuPages
|
|
10
|
+
TEXT = PauseMenu::TEXT
|
|
11
|
+
DIM = PauseMenu::DIM
|
|
12
|
+
LINE = 12
|
|
13
|
+
COLUMNS = 25
|
|
14
|
+
ROW_WIDTH = 200
|
|
15
|
+
|
|
16
|
+
WARNING = 0xff7a6b
|
|
17
|
+
|
|
18
|
+
PORT_DEVICES = %i[joystick mouse paddles].freeze
|
|
19
|
+
PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
|
|
20
|
+
PORT_ACTIONS = [%i[port1_joystick port1_mouse port1_paddles], %i[port2_joystick port2_mouse port2_paddles]].freeze
|
|
21
|
+
POTS = [%i[none mouse1 paddles1], %i[none mouse2 paddles2]].freeze
|
|
22
|
+
|
|
23
|
+
# The disks, tapes and cartridges the pages put in and take out.
|
|
24
|
+
attr_reader :media
|
|
25
|
+
|
|
26
|
+
def initialize(painter, buttons, disk_path, options)
|
|
27
|
+
@painter = painter
|
|
28
|
+
@buttons = buttons
|
|
29
|
+
@media = MenuMedia.new(disk_path, options.writable?)
|
|
30
|
+
@reu_kb = options.reu
|
|
31
|
+
@computer = nil
|
|
32
|
+
@controls = nil
|
|
33
|
+
@sound = nil
|
|
34
|
+
@left = 0
|
|
35
|
+
@top = 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The machine the pages show and change.
|
|
39
|
+
def machine(computer, controls, sound)
|
|
40
|
+
@computer = computer
|
|
41
|
+
@media.computer = computer
|
|
42
|
+
@controls = controls
|
|
43
|
+
@sound = sound
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def draw(page, left, top)
|
|
47
|
+
problem = @media.problem
|
|
48
|
+
@painter.text(left, top + 186, problem[0, COLUMNS], WARNING) unless problem.empty?
|
|
49
|
+
@left = left
|
|
50
|
+
@top = top
|
|
51
|
+
case page
|
|
52
|
+
when :drive then draw_drive
|
|
53
|
+
when :datasette then draw_datasette
|
|
54
|
+
when :expansion then draw_expansion
|
|
55
|
+
when :ports then draw_ports
|
|
56
|
+
when :sound then draw_sound
|
|
57
|
+
else draw_power
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def perform(action)
|
|
62
|
+
case action
|
|
63
|
+
when :eject_disk, :previous_disk, :next_disk, :protect, :unprotect then @media.drive(action)
|
|
64
|
+
when :tape_stop then @computer.datasette.stop!
|
|
65
|
+
when :tape_down then @computer.datasette.play!
|
|
66
|
+
when :tape_rewind then @computer.datasette.rewind
|
|
67
|
+
when :tape_eject then @computer.datasette.eject
|
|
68
|
+
when :keys_c64, :keys_joystick then keys(action == :keys_c64 ? :keyboard : :joystick)
|
|
69
|
+
when :swap then @controls.swap_ports
|
|
70
|
+
when :sound_on, :mute then @sound.toggle_mute unless @sound.muted? == (action == :mute)
|
|
71
|
+
when :sid6581, :sid8580 then @computer.sid.model = action == :sid8580 ? :mos8580 : :mos6581
|
|
72
|
+
when :reset then @computer.reset!
|
|
73
|
+
when :power_cycle then @computer.power_cycle!
|
|
74
|
+
else pick_port(action)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def draw_drive
|
|
81
|
+
media = @media
|
|
82
|
+
inserted = media.inserted?
|
|
83
|
+
set = inserted ? media.disk_set : []
|
|
84
|
+
place = set.size < 2 ? "" : " #{set.index(media.disk_path).to_i + 1} OF #{set.size}"
|
|
85
|
+
info("DISK#{place}", inserted ? File.basename(media.disk_path) : "")
|
|
86
|
+
row("INSERT...", :insert_disk)
|
|
87
|
+
row("EJECT", :eject_disk)
|
|
88
|
+
unless set.size < 2
|
|
89
|
+
row("PREVIOUS DISK", :previous_disk)
|
|
90
|
+
row("NEXT DISK", :next_disk)
|
|
91
|
+
end
|
|
92
|
+
toggle("WRITABLE", :writable, [%w[OFF ON], %i[protect unprotect], media.writable? ? 1 : 0])
|
|
93
|
+
skip
|
|
94
|
+
info("DEVICE", @computer.drive1541.nil? ? "FAST LOADING" : "1541, TRUE DRIVE")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def draw_datasette
|
|
98
|
+
datasette = @computer.datasette
|
|
99
|
+
tape = datasette.tape
|
|
100
|
+
info("TAPE", tape.nil? ? "" : File.basename(tape.path))
|
|
101
|
+
row("INSERT...", :insert_tape)
|
|
102
|
+
row("EJECT", :tape_eject)
|
|
103
|
+
toggle("PLAY", :tape_play, [%w[UP DOWN], %i[tape_stop tape_down], datasette.playing? ? 1 : 0])
|
|
104
|
+
row("REWIND", :tape_rewind)
|
|
105
|
+
skip
|
|
106
|
+
info("MOTOR", datasette.motor? ? "ON" : "OFF")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def draw_expansion
|
|
110
|
+
cartridge = @computer.address_bus.cartridge
|
|
111
|
+
info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
|
|
112
|
+
row("INSERT...", :insert_cartridge)
|
|
113
|
+
row("REMOVE", :remove_cartridge)
|
|
114
|
+
row("FREEZE", :freeze) if !cartridge.nil? && cartridge.freezer?
|
|
115
|
+
return if @reu_kb.nil?
|
|
116
|
+
|
|
117
|
+
skip
|
|
118
|
+
info("RAM EXPANSION", "REU, #{@reu_kb} KB")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def draw_ports
|
|
122
|
+
toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
|
|
123
|
+
toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
|
|
124
|
+
joystick = @controls.joystick_mode?
|
|
125
|
+
toggle("KEYS", :keys, [%w[C64 JOYSTICK], %i[keys_c64 keys_joystick], joystick ? 1 : 0])
|
|
126
|
+
return unless joystick
|
|
127
|
+
|
|
128
|
+
row("SWAP JOYSTICKS", :swap)
|
|
129
|
+
skip
|
|
130
|
+
arrows = @controls.arrows_port
|
|
131
|
+
info("ARROWS, SPACE", "PORT #{arrows}")
|
|
132
|
+
info("WASD, LEFT SHIFT", "PORT #{3 - arrows}")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def draw_sound
|
|
136
|
+
if @sound.on?
|
|
137
|
+
toggle("OUTPUT", :output, [%w[ON MUTED], %i[sound_on mute], @sound.muted? ? 1 : 0])
|
|
138
|
+
else
|
|
139
|
+
info("OUTPUT", "OFF")
|
|
140
|
+
end
|
|
141
|
+
toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], @computer.sid.model == :mos8580 ? 1 : 0])
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def draw_power
|
|
145
|
+
row("RESET", :reset)
|
|
146
|
+
row("POWER CYCLE", :power_cycle)
|
|
147
|
+
skip
|
|
148
|
+
row("QUIT", :quit)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# A label, and its value under it, or NONE in the dim colour when the
|
|
152
|
+
# value is empty.
|
|
153
|
+
def info(label, value)
|
|
154
|
+
@painter.text(@left, @top, label, DIM)
|
|
155
|
+
if value.empty?
|
|
156
|
+
@painter.text(@left, @top + LINE, "NONE", DIM)
|
|
157
|
+
else
|
|
158
|
+
@painter.text(@left, @top + LINE, Painter.fit(value, COLUMNS), TEXT)
|
|
159
|
+
end
|
|
160
|
+
@top += (LINE * 2) + 6
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def row(label, action, value = "")
|
|
164
|
+
@buttons.row([@left, @top, ROW_WIDTH], label, action, value)
|
|
165
|
+
@top += Buttons::HEIGHT + 2
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def toggle(label, action, choices)
|
|
169
|
+
@buttons.toggle([@left, @top, ROW_WIDTH], label, action, choices)
|
|
170
|
+
@top += Buttons::HEIGHT + 2
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def skip
|
|
174
|
+
@top += LINE
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def keys(mode) = @controls.joystick_mode = mode == :joystick
|
|
178
|
+
|
|
179
|
+
def port_device(port)
|
|
180
|
+
index = POTS[port - 1].index(@controls.pot)
|
|
181
|
+
index.nil? ? :joystick : PORT_DEVICES[index]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Plugs the device a choice such as :port1_mouse names into its port.
|
|
185
|
+
def pick_port(action)
|
|
186
|
+
index = PORT_ACTIONS[0].include?(action) ? 0 : 1
|
|
187
|
+
choice = PORT_ACTIONS[index].index(action)
|
|
188
|
+
plug_device(index + 1, choice) unless choice.nil? || choice == PORT_DEVICES.index(port_device(index + 1))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Plugs PORT_DEVICES[choice] into the port. A joystick there takes out
|
|
192
|
+
# the pot device.
|
|
193
|
+
def plug_device(port, choice)
|
|
194
|
+
@controls.plug(POTS[port - 1][choice])
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -95,6 +95,16 @@ module Badline
|
|
|
95
95
|
|
|
96
96
|
def self.width(string, scale: 1) = string.length * GLYPH * scale
|
|
97
97
|
|
|
98
|
+
# The string cut to `columns` characters in its middle, so both its
|
|
99
|
+
# start and its end show.
|
|
100
|
+
def self.fit(string, columns)
|
|
101
|
+
return string if string.length <= columns
|
|
102
|
+
|
|
103
|
+
head = (columns - 3) / 2
|
|
104
|
+
tail = columns - 3 - head
|
|
105
|
+
"#{string[0, head]}...#{string[string.length - tail, tail]}"
|
|
106
|
+
end
|
|
107
|
+
|
|
98
108
|
def icon(name, left, top, rgb, scale: 1)
|
|
99
109
|
tint(rgb)
|
|
100
110
|
place(top, GLYPH * scale)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The emulator's pause menu, which F9 opens over the frozen picture.
|
|
6
|
+
# The machine stands still while it's open. Its sections follow the
|
|
7
|
+
# machine's hardware: the drive, the datasette, the expansion port, the
|
|
8
|
+
# control ports, the sound and the power. It's drawn in colours outside
|
|
9
|
+
# the C64's palette, so it reads as the emulator's and not the
|
|
10
|
+
# machine's, and F9 or Esc closes it again.
|
|
11
|
+
#
|
|
12
|
+
# INSERT on a page opens the file browser for that device. Quick open
|
|
13
|
+
# starts a file as the command line does, in a new machine, and it and
|
|
14
|
+
# a cartridge going in or out ask first, as each power cycles the
|
|
15
|
+
# machine.
|
|
16
|
+
class PauseMenu
|
|
17
|
+
SECTIONS = ["DRIVE 8", "DATASETTE", "EXPANSION PORT", "PORTS", "SOUND", "POWER"].freeze
|
|
18
|
+
PAGES = %i[drive datasette expansion ports sound power].freeze
|
|
19
|
+
DISKS = %w[.d64 .d71 .d81 .g64 .t64].freeze
|
|
20
|
+
|
|
21
|
+
PANEL = 0x1d2230
|
|
22
|
+
EDGE = 0x3a4256
|
|
23
|
+
TEXT = 0xc9d1e0
|
|
24
|
+
BRIGHT = 0xffc66d
|
|
25
|
+
DIM = 0x6b7489
|
|
26
|
+
FILL = 0x343c50
|
|
27
|
+
|
|
28
|
+
LEFT = 16
|
|
29
|
+
TOP = 16
|
|
30
|
+
WIDTH = Screen::WIDTH - 32
|
|
31
|
+
HEIGHT = Screen::HEIGHT - 32
|
|
32
|
+
BODY = 154
|
|
33
|
+
|
|
34
|
+
ESCAPE = 41
|
|
35
|
+
RETURN = 40
|
|
36
|
+
SPACE = 44
|
|
37
|
+
ARROWS = { 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
|
|
38
|
+
MOUSEWHEEL = 0x403
|
|
39
|
+
|
|
40
|
+
# Takes the media, the REU and whether disks are writable from Options.
|
|
41
|
+
def initialize(painter, options)
|
|
42
|
+
media_path = options.media_path.to_s
|
|
43
|
+
@painter = painter
|
|
44
|
+
@buttons = Buttons.new(painter, [TEXT, BRIGHT, PANEL, FILL])
|
|
45
|
+
@pages = MenuPages.new(painter, @buttons, PauseMenu.disk_path(media_path), options)
|
|
46
|
+
@dialogs = MenuDialogs.new(painter, @buttons, @pages.media, options)
|
|
47
|
+
@open = false
|
|
48
|
+
@section = 0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The path of the disk the command line puts in device 8, or an empty
|
|
52
|
+
# one for other media.
|
|
53
|
+
def self.disk_path(path)
|
|
54
|
+
return "" if path.empty?
|
|
55
|
+
return path if File.directory?(path)
|
|
56
|
+
|
|
57
|
+
DISKS.include?(File.extname(path).downcase) ? path : ""
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def open? = @open
|
|
61
|
+
|
|
62
|
+
# The machine Quick open started, which the app runs in place of the
|
|
63
|
+
# one before once #frame returns :swap.
|
|
64
|
+
def computer = @dialogs.computer
|
|
65
|
+
|
|
66
|
+
def show(computer, controls, sound)
|
|
67
|
+
@open = true
|
|
68
|
+
@buttons.focus = PAGES[@section]
|
|
69
|
+
@pages.machine(computer, controls, sound)
|
|
70
|
+
SDL.SDL_SetRelativeMouseMode(0)
|
|
71
|
+
draw
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close
|
|
75
|
+
@open = false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Opens on the file the SDL event says was dropped on the window,
|
|
79
|
+
# and says whether the menu stays open to ask about it: a disk or tape
|
|
80
|
+
# goes straight in, and anything else asks first.
|
|
81
|
+
def drop(computer, controls, sound)
|
|
82
|
+
show(computer, controls, sound)
|
|
83
|
+
file = SDL.event_file(SDL.event)
|
|
84
|
+
asks = @dialogs.drop(LibC.strstr(file, ""))
|
|
85
|
+
LibC.free(file)
|
|
86
|
+
asks
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Handles a key pressed while the menu is open: the arrows move
|
|
90
|
+
# between the buttons, and a section opens as they reach it, Return
|
|
91
|
+
# or Space presses the button, and F9 or Esc closes the menu. While
|
|
92
|
+
# the file browser or a question is open, the keys go to it, and Esc
|
|
93
|
+
# goes back to the page. Returns the action the app has to take on,
|
|
94
|
+
# as #click does.
|
|
95
|
+
def key(scancode)
|
|
96
|
+
return :resume if scancode == Keys::F9
|
|
97
|
+
return @dialogs.key(scancode) if @dialogs.open?
|
|
98
|
+
return :resume if scancode == ESCAPE
|
|
99
|
+
return choose(@buttons.focus) if [RETURN, SPACE].include?(scancode)
|
|
100
|
+
|
|
101
|
+
direction = ARROWS[scancode]
|
|
102
|
+
move(direction) unless direction.nil?
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Handles a click, and returns the action the app has to take on, if
|
|
107
|
+
# any: :resume, :quit, :freeze or :swap.
|
|
108
|
+
def click(left, top)
|
|
109
|
+
action = @buttons.action_at(left, top)
|
|
110
|
+
return @dialogs.click(action, top) if %i[browse confirm cancel].include?(action)
|
|
111
|
+
|
|
112
|
+
choose(action)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Handles the events that came in, draws the menu over the frozen
|
|
116
|
+
# picture, dimmed, and returns the action the app has to take on, if
|
|
117
|
+
# any, leaving the events after one for the machine. Key releases
|
|
118
|
+
# still reach the machine, so no key stays held down while it stands
|
|
119
|
+
# still.
|
|
120
|
+
def frame(renderer, texture, controls)
|
|
121
|
+
action = nil
|
|
122
|
+
action = event(SDL.event_type(SDL.event), controls) while action.nil? && SDL.SDL_PollEvent(SDL.event) != 0
|
|
123
|
+
SDL.SDL_SetTextureColorMod(texture, 96, 96, 96)
|
|
124
|
+
SDL.SDL_RenderClear(renderer)
|
|
125
|
+
SDL.SDL_RenderCopy(renderer, texture, nil, nil)
|
|
126
|
+
SDL.SDL_SetTextureColorMod(texture, 255, 255, 255)
|
|
127
|
+
draw
|
|
128
|
+
SDL.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255)
|
|
129
|
+
SDL.SDL_RenderPresent(renderer)
|
|
130
|
+
SDL.SDL_Delay(10)
|
|
131
|
+
action
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def draw
|
|
135
|
+
painter = @painter
|
|
136
|
+
@buttons.forget
|
|
137
|
+
painter.box(LEFT, TOP, WIDTH, HEIGHT, PANEL)
|
|
138
|
+
painter.box(LEFT, TOP + 24, WIDTH, 1, EDGE)
|
|
139
|
+
painter.text(LEFT + 8, TOP + 9, "PAUSED", BRIGHT)
|
|
140
|
+
resume = "RESUME"
|
|
141
|
+
@buttons.plain(LEFT + WIDTH - 8 - Painter.width(resume) - (Buttons::PAD * 2), TOP + 7, resume, :resume)
|
|
142
|
+
return @dialogs.draw(LEFT + 8, TOP + 34, WIDTH - 16) if @dialogs.open?
|
|
143
|
+
|
|
144
|
+
painter.box(BODY - 8, TOP + 32, 1, HEIGHT - 56, EDGE)
|
|
145
|
+
draw_sections
|
|
146
|
+
@buttons.plain(LEFT + 6, TOP + HEIGHT - 28, "QUICK OPEN...", :quick_open)
|
|
147
|
+
painter.text(LEFT + 8, TOP + HEIGHT - 12, "F9/ESC: RESUME", DIM)
|
|
148
|
+
@pages.draw(PAGES[@section], BODY, TOP + 34)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def choose(pressed)
|
|
154
|
+
return nil if pressed.nil?
|
|
155
|
+
|
|
156
|
+
action = @buttons.resolve(pressed)
|
|
157
|
+
index = PAGES.index(action)
|
|
158
|
+
unless index.nil?
|
|
159
|
+
@section = index
|
|
160
|
+
return nil
|
|
161
|
+
end
|
|
162
|
+
return action if %i[resume quit freeze].include?(action)
|
|
163
|
+
|
|
164
|
+
menu_action(action)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def menu_action(action)
|
|
168
|
+
case action
|
|
169
|
+
when :insert_disk then @dialogs.browse(:disk)
|
|
170
|
+
when :insert_tape then @dialogs.browse(:tape)
|
|
171
|
+
when :insert_cartridge then @dialogs.browse(:cartridge)
|
|
172
|
+
when :quick_open then @dialogs.browse(:program)
|
|
173
|
+
when :remove_cartridge then @dialogs.ask_remove
|
|
174
|
+
else @pages.perform(action)
|
|
175
|
+
end
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Moves the focus: down from RESUME to the sections, through them,
|
|
180
|
+
# where each opens as it's reached, on to Quick open, right into a
|
|
181
|
+
# page, up and down it, and left out of it again.
|
|
182
|
+
def move(direction)
|
|
183
|
+
focus = @buttons.focus
|
|
184
|
+
section = focus == :quick_open ? PAGES.size : PAGES.index(focus)
|
|
185
|
+
if focus == :resume
|
|
186
|
+
@buttons.focus = PAGES[@section] if %i[down left].include?(direction)
|
|
187
|
+
elsif section.nil?
|
|
188
|
+
move_in_page(direction)
|
|
189
|
+
else
|
|
190
|
+
move_in_sections(section, direction)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Up and down go through the page's rows, and left goes back to its
|
|
195
|
+
# section.
|
|
196
|
+
def move_in_page(direction)
|
|
197
|
+
case direction
|
|
198
|
+
when :up, :down then @buttons.shift(direction == :up ? -1 : 1, PAGES.size + 2)
|
|
199
|
+
when :left then @buttons.focus = PAGES[@section]
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def move_in_sections(section, direction)
|
|
204
|
+
case direction
|
|
205
|
+
when :up then section.zero? ? @buttons.focus = :resume : open_section(section - 1)
|
|
206
|
+
when :down then section < PAGES.size - 1 ? open_section(section + 1) : @buttons.focus = :quick_open
|
|
207
|
+
when :right then @buttons.focus = @buttons.action(PAGES.size + 2) || @buttons.focus if section < PAGES.size
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Opens a section, laid out at once, so keys that come before the next
|
|
212
|
+
# frame find its rows.
|
|
213
|
+
def open_section(index)
|
|
214
|
+
@section = index
|
|
215
|
+
@buttons.focus = PAGES[index]
|
|
216
|
+
draw
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def event(type, controls)
|
|
220
|
+
event = SDL.event
|
|
221
|
+
case type
|
|
222
|
+
when SDL::QUIT then :quit
|
|
223
|
+
when SDL::KEYDOWN then key_down(SDL.event_scancode(event), SDL.event_repeat(event))
|
|
224
|
+
when SDL::KEYUP then controls.key(SDL.event_scancode(event), false) && nil
|
|
225
|
+
when SDL::MOUSEMOTION then @buttons.point(SDL.event_x(event), SDL.event_y(event)) && nil
|
|
226
|
+
when SDL::MOUSEBUTTONDOWN
|
|
227
|
+
SDL.event_button(event) == 1 ? click(SDL.event_x(event), SDL.event_y(event)) : nil
|
|
228
|
+
when MOUSEWHEEL then @dialogs.scroll(-3 * SDL.event_x(event)) && nil
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# A key held down repeats only the arrows.
|
|
233
|
+
def key_down(scancode, repeat)
|
|
234
|
+
repeat.zero? || ARROWS.key?(scancode) ? key(scancode) : nil
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def draw_sections
|
|
238
|
+
top = TOP + 32
|
|
239
|
+
SECTIONS.each_with_index do |name, index|
|
|
240
|
+
@buttons.plain(LEFT + 6, top, name.ljust(14), PAGES[index], on: index == @section)
|
|
241
|
+
top += Buttons::HEIGHT + 2
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -15,7 +15,7 @@ module Badline
|
|
|
15
15
|
unless options.screenshot.empty? || options.frames.zero?
|
|
16
16
|
@events += [Options::Event.new(options.frames, "screenshot", options.screenshot)]
|
|
17
17
|
end
|
|
18
|
-
@read_only = options.
|
|
18
|
+
@read_only = !options.writable?
|
|
19
19
|
@program = options.program
|
|
20
20
|
@releases = []
|
|
21
21
|
@failed = false
|
|
@@ -63,6 +63,10 @@ module Badline
|
|
|
63
63
|
.map { |event| Timeline.path(event.argument, frame) }
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Presses the freeze button after `frame`, and lets go of it as the
|
|
67
|
+
# events do.
|
|
68
|
+
def press_freeze(computer, frame) = freeze(computer, Options::Event.new(frame, "freeze", ""))
|
|
69
|
+
|
|
66
70
|
# Whether the run ends at the frame: at a quit, or once an event failed.
|
|
67
71
|
def quit?(frame) = @failed || @events.any? { |event| event.frame == frame && event.action == "quit" }
|
|
68
72
|
|
data/lib/badline/frontend.rb
CHANGED
|
@@ -11,6 +11,7 @@ require "badline/frontend/screenshot"
|
|
|
11
11
|
require "badline/frontend/timeline"
|
|
12
12
|
require "badline/frontend/sound"
|
|
13
13
|
require "badline/frontend/pacer"
|
|
14
|
+
require "badline/frontend/frame_report"
|
|
14
15
|
require "badline/frontend/controls"
|
|
15
16
|
require "badline/frontend/pad_port"
|
|
16
17
|
require "badline/frontend/gamepads"
|
|
@@ -30,3 +31,9 @@ require "badline/frontend/info_view"
|
|
|
30
31
|
require "badline/frontend/player_commands"
|
|
31
32
|
require "badline/frontend/player_screen"
|
|
32
33
|
require "badline/frontend/player_window"
|
|
34
|
+
require "badline/frontend/pause_menu"
|
|
35
|
+
require "badline/frontend/file_browser"
|
|
36
|
+
require "badline/frontend/confirmation"
|
|
37
|
+
require "badline/frontend/menu_dialogs"
|
|
38
|
+
require "badline/frontend/menu_media"
|
|
39
|
+
require "badline/frontend/menu_pages"
|