bzr branch
http://9ix.org/bzr/spacey
1
by Josh C
zoetrope 1.4 |
1 |
-- Class: DebugConsole |
2 |
-- It can be used to keep track of fps, the position of a sprite, |
|
3 |
-- and so on. It only updates when visible. |
|
4 |
-- |
|
5 |
-- This also allows debugging hotkeys -- e.g. you could set it so that |
|
6 |
-- pressing Control-Alt-I toggles invincibility of the player sprite. |
|
7 |
-- Out of the box: |
|
8 |
-- - Control-Alt-F toggles fullscreen |
|
9 |
-- - Control-Alt-Q quits the app. |
|
10 |
-- - Control-Alt-P deactivates the view. |
|
11 |
-- - Control-Alt-R reloads all app code from on disk. |
|
12 |
-- - Control-Alt-S saves a screenshot to the app's directory -- |
|
13 |
-- see https://love2d.org/wiki/love.filesystem for where this is. |
|
14 |
||
15 |
DebugConsole = Group:extend |
|
16 |
{ |
|
17 |
-- Property: toggleKey |
|
18 |
-- What key toggles visibility. By default, this is the tab key. |
|
19 |
toggleKey = 'tab', |
|
20 |
||
21 |
-- Property: hotkeyModifiers |
|
22 |
-- A table of modifier keys that must be held in order to activate |
|
23 |
-- a debugging hotkey (set via <addHotkey()>). If you want hotkeys to |
|
24 |
-- activate without having to hold any keys down, set this to nil. |
|
25 |
hotkeyModifiers = {'ctrl', 'alt'}, |
|
26 |
||
27 |
-- Property: watchBasics |
|
28 |
-- If true, the console will automatically start watching the frames |
|
29 |
-- per second and memory usage. Changing this value after the object has |
|
30 |
-- been created has no effect. |
|
31 |
watchBasics = true, |
|
32 |
||
33 |
-- Property: watchWidth |
|
34 |
-- How wide the sidebar, where watch values are displayed, should be. |
|
35 |
watchWidth = 150, |
|
36 |
||
37 |
-- Property: inputHistory |
|
38 |
-- A table of previously-entered commands. |
|
39 |
inputHistory = {}, |
|
40 |
||
41 |
-- Property: inputHistoryIndex |
|
42 |
-- Which history entry, if any, we are displaying. |
|
43 |
inputHistoryIndex = 1, |
|
44 |
||
45 |
-- Property: bg |
|
46 |
-- The background <Fill> used to darken the view. |
|
47 |
||
48 |
-- Property: log |
|
49 |
-- The <Text> sprite showing recent lines in the log. |
|
50 |
||
51 |
-- Property: watchList |
|
52 |
-- The <Text> sprite showing the state of all watched variables. |
|
53 |
||
54 |
-- Property: input |
|
55 |
-- The <TextInput> that the user types into to enter commands. |
|
56 |
||
57 |
-- Property: prompt |
|
58 |
-- The <Text> sprite that shows a > in front of commands. |
|
59 |
||
60 |
-- internal property: _bindings |
|
61 |
-- Keeps track of debugging hotkeys. |
|
62 |
||
63 |
new = function (self, obj) |
|
64 |
local width = the.app.width |
|
65 |
local height = the.app.height |
|
66 |
||
67 |
obj = self:extend(obj) |
|
68 |
||
69 |
obj.visible = false |
|
70 |
obj._watches = {} |
|
71 |
obj._hotkeys = {} |
|
72 |
||
73 |
obj.fill = Fill:new{ x = 0, y = 0, width = width, height = height, fill = {0, 0, 0, 200} } |
|
74 |
obj:add(obj.fill) |
|
75 |
||
76 |
obj.log = Text:new{ x = 4, y = 4, width = width - self.watchWidth - 8, height = height - 8, text = '' } |
|
77 |
obj:add(obj.log) |
|
78 |
||
79 |
obj.watchList = Text:new{ x = width - self.watchWidth - 4, y = 4, |
|
80 |
width = self.watchWidth - 8, height = height - 8, text = '', wordWrap = false } |
|
81 |
obj:add(obj.watchList) |
|
82 |
||
83 |
obj.prompt = Text:new{ x = 4, y = 0, width = 100, text = '>' } |
|
84 |
obj:add(obj.prompt) |
|
85 |
||
86 |
local inputIndent = obj.log._fontObj:getWidth('>') + 4 |
|
87 |
obj.input = TextInput:new |
|
88 |
{ |
|
89 |
x = inputIndent, y = 0, width = the.app.width, |
|
90 |
active = false, |
|
91 |
onType = function (self, char) |
|
92 |
return char ~= the.console.toggleKey |
|
93 |
end |
|
94 |
} |
|
95 |
obj:add(obj.input) |
|
96 |
||
97 |
-- some default behavior |
|
98 |
||
99 |
obj:addHotkey('f', function() the.app:toggleFullscreen() end) |
|
100 |
obj:addHotkey('p', function() |
|
101 |
the.view.active = not the.view.active |
|
102 |
if the.view.active then |
|
103 |
the.view:tint() |
|
104 |
else |
|
105 |
the.view:tint(0, 0, 0, 200) |
|
106 |
end |
|
107 |
end) |
|
108 |
obj:addHotkey('q', love.event.quit) |
|
109 |
if debugger then obj:addHotkey('r', debugger.reload) end |
|
110 |
obj:addHotkey('s', function() the.app:saveScreenshot('screenshot.png') end) |
|
111 |
||
112 |
if obj.watchBasics then |
|
113 |
obj:watch('FPS', 'love.timer.getFPS()') |
|
114 |
obj:watch('Memory', 'math.floor(collectgarbage("count") / 1024) .. "M"') |
|
115 |
end |
|
116 |
||
117 |
-- hijack print function |
|
118 |
-- this is nasty to debug if it goes wrong, be careful |
|
119 |
||
120 |
obj._oldPrint = print |
|
121 |
print = function (...) |
|
122 |
local caller = debug.getinfo(2) |
|
123 |
||
124 |
if caller.linedefined ~= 0 then |
|
125 |
obj.log.text = obj.log.text .. '(' .. caller.short_src .. ':' .. caller.linedefined .. ') ' |
|
126 |
end |
|
127 |
||
128 |
for _, value in pairs{...} do |
|
129 |
obj.log.text = obj.log.text .. tostring(value) .. ' ' |
|
130 |
end |
|
131 |
||
132 |
obj.log.text = obj.log.text .. '\n' |
|
133 |
obj._updateLog = true |
|
134 |
obj._oldPrint(...) |
|
135 |
end |
|
136 |
||
137 |
debugger._unsourcedPrint = function (...) |
|
138 |
for _, value in pairs{...} do |
|
139 |
obj.log.text = obj.log.text .. tostring(value) .. ' ' |
|
140 |
end |
|
141 |
||
142 |
obj.log.text = obj.log.text .. '\n' |
|
143 |
obj._updateLog = true |
|
144 |
obj._oldPrint(...) |
|
145 |
end |
|
146 |
||
147 |
the.console = obj |
|
148 |
if obj.onNew then obj.onNew() end |
|
149 |
return obj |
|
150 |
end, |
|
151 |
||
152 |
-- Method: watch |
|
153 |
-- Adds an expression to be watched. |
|
154 |
-- |
|
155 |
-- Arguments: |
|
156 |
-- label - string label |
|
157 |
-- expression - expression to evaluate as a string |
|
158 |
||
159 |
watch = function (self, label, expression) |
|
160 |
table.insert(self._watches, { label = label, |
|
161 |
func = loadstring('return ' .. expression) }) |
|
162 |
end, |
|
163 |
||
164 |
-- Method: addHotkey |
|
165 |
-- Adds a hotkey to execute a function. This hotkey will require |
|
166 |
-- holding down whatever modifiers are set in <hotkeyModifiers>. |
|
167 |
-- |
|
168 |
-- Arguments: |
|
169 |
-- key - key to trigger the hotkey |
|
170 |
-- func - function to run. This will receive the key that |
|
171 |
-- was pressed, so you can re-use functions (i.e. |
|
172 |
-- the 1 key loads level 1, the 2 key loads level 2). |
|
173 |
-- |
|
174 |
-- Returns: |
|
175 |
-- nothing |
|
176 |
||
177 |
addHotkey = function (self, key, func) |
|
178 |
table.insert(self._hotkeys, { key = key, func = func }) |
|
179 |
end, |
|
180 |
||
181 |
-- Method: execute |
|
182 |
-- Safely executes a string of code and prints the result. |
|
183 |
-- |
|
184 |
-- Arguments: |
|
185 |
-- code - string code to execute |
|
186 |
-- |
|
187 |
-- Returns: |
|
188 |
-- string result |
|
189 |
||
190 |
execute = function (self, code) |
|
191 |
if string.sub(code, 1, 1) == '=' then |
|
192 |
code = 'debugger._unsourcedPrint (' .. string.sub(code, 2) .. ')' |
|
193 |
end |
|
194 |
||
195 |
local func, err = loadstring(code) |
|
196 |
||
197 |
if func then |
|
198 |
local ok, result = pcall(func) |
|
199 |
||
200 |
if not ok then |
|
201 |
debugger._unsourcedPrint('Error, ' .. tostring(result) .. '\n') |
|
202 |
else |
|
203 |
debugger._unsourcedPrint('') |
|
204 |
end |
|
205 |
||
206 |
return tostring(result) |
|
207 |
else |
|
208 |
debugger._unsourcedPrint('Syntax error, ' .. string.gsub(tostring(err), '^.*:', '') .. '\n') |
|
209 |
end |
|
210 |
end, |
|
211 |
||
212 |
-- Method: show |
|
213 |
-- Shows the debug console. |
|
214 |
-- |
|
215 |
-- Arguments: |
|
216 |
-- none |
|
217 |
-- |
|
218 |
-- Returns: |
|
219 |
-- nothing |
|
220 |
||
221 |
show = function (self) |
|
222 |
self.visible = true |
|
223 |
self.input.active = true |
|
224 |
end, |
|
225 |
||
226 |
-- Method: hide |
|
227 |
-- Hides the debug console. |
|
228 |
-- |
|
229 |
-- Arguments: |
|
230 |
-- none |
|
231 |
-- |
|
232 |
-- Returns: |
|
233 |
-- nothing |
|
234 |
||
235 |
hide = function (self) |
|
236 |
self.visible = false |
|
237 |
self.input.active = false |
|
238 |
end, |
|
239 |
||
240 |
update = function (self, elapsed) |
|
241 |
-- listen for visibility key |
|
242 |
||
243 |
if the.keys:justPressed(self.toggleKey) then |
|
244 |
self.visible = not self.visible |
|
245 |
self.input.active = self.visible |
|
246 |
end |
|
247 |
||
248 |
-- listen for hotkeys |
|
249 |
||
250 |
local modifiers = (self.hotkeyModifiers == nil) |
|
251 |
||
252 |
if not modifiers then |
|
253 |
modifiers = true |
|
254 |
||
255 |
for _, key in pairs(self.hotkeyModifiers) do |
|
256 |
if not the.keys:pressed(key) then |
|
257 |
modifiers = false |
|
258 |
break |
|
259 |
end |
|
260 |
end |
|
261 |
end |
|
262 |
||
263 |
if modifiers then |
|
264 |
for _, hotkey in pairs(self._hotkeys) do |
|
265 |
if the.keys:justPressed(hotkey.key) then |
|
266 |
hotkey.func(hotkey.key) |
|
267 |
end |
|
268 |
end |
|
269 |
end |
|
270 |
||
271 |
if self.visible then |
|
272 |
-- update watches |
|
273 |
||
274 |
self.watchList.text = '' |
|
275 |
||
276 |
for _, watch in pairs(self._watches) do |
|
277 |
local ok, value = pcall(watch.func) |
|
278 |
if not ok then value = nil end |
|
279 |
||
280 |
self.watchList.text = self.watchList.text .. watch.label .. ': ' .. tostring(value) .. '\n' |
|
281 |
end |
|
282 |
||
283 |
-- update log |
|
284 |
||
285 |
if self._updateLog then |
|
286 |
local lineHeight = self.log._fontObj:getHeight() |
|
287 |
local _, height = self.log:getSize() |
|
288 |
local linesToDelete = math.ceil((height - the.app.height - 20) / lineHeight) |
|
289 |
||
290 |
if linesToDelete > 0 then |
|
291 |
self.log.text = string.gsub(self.log.text, '.-\n', '', linesToDelete) |
|
292 |
height = height - linesToDelete * lineHeight |
|
293 |
end |
|
294 |
||
295 |
self.prompt.y = height + 4 |
|
296 |
self.input.y = height + 4 |
|
297 |
self._updateLog = false |
|
298 |
end |
|
299 |
||
300 |
-- handle special keys at the console |
|
301 |
||
302 |
if the.keys:pressed('ctrl') and the.keys:justPressed('a') then |
|
303 |
self.input.caret = 0 |
|
304 |
end |
|
305 |
||
306 |
if the.keys:pressed('ctrl') and the.keys:justPressed('e') then |
|
307 |
self.input.caret = string.len(self.input.text) |
|
308 |
end |
|
309 |
||
310 |
if the.keys:pressed('ctrl') and the.keys:justPressed('k') then |
|
311 |
self.input.caret = 0 |
|
312 |
self.input.text = '' |
|
313 |
end |
|
314 |
||
315 |
if the.keys:justPressed('up') and self.inputHistoryIndex > 1 then |
|
316 |
-- save what the user was in the middle of typing |
|
317 |
||
318 |
self.inputHistory[self.inputHistoryIndex] = self.input.text |
|
319 |
||
320 |
self.input.text = self.inputHistory[self.inputHistoryIndex - 1] |
|
321 |
self.input.caret = string.len(self.input.text) |
|
322 |
self.inputHistoryIndex = self.inputHistoryIndex - 1 |
|
323 |
end |
|
324 |
||
325 |
if the.keys:justPressed('down') and self.inputHistoryIndex < #self.inputHistory then |
|
326 |
self.input.text = self.inputHistory[self.inputHistoryIndex + 1] |
|
327 |
self.input.caret = string.len(self.input.text) |
|
328 |
self.inputHistoryIndex = self.inputHistoryIndex + 1 |
|
329 |
end |
|
330 |
||
331 |
if the.keys:justPressed('return') then |
|
332 |
debugger._unsourcedPrint('>' .. self.input.text) |
|
333 |
self:execute(self.input.text) |
|
334 |
table.insert(self.inputHistory, self.inputHistoryIndex, self.input.text) |
|
335 |
||
336 |
while #self.inputHistory > self.inputHistoryIndex do |
|
337 |
table.remove(self.inputHistory) |
|
338 |
end |
|
339 |
||
340 |
self.inputHistoryIndex = self.inputHistoryIndex + 1 |
|
341 |
self.input.text = '' |
|
342 |
self.input.caret = 0 |
|
343 |
end |
|
344 |
end |
|
345 |
||
346 |
Group.update(self, elapsed) |
|
347 |
end |
|
348 |
} |
|
349 |
||
350 |
-- Function: debugger.reload |
|
351 |
-- Resets the entire app and forces all code to be reloaded from |
|
352 |
-- on disk. via https://love2d.org/forums/viewtopic.php?f=3&t=7965 |
|
353 |
-- |
|
354 |
-- Arguments: |
|
355 |
-- none |
|
356 |
-- |
|
357 |
-- Returns: |
|
358 |
-- nothing |
|
359 |
||
360 |
if debugger then |
|
361 |
debugger.reload = function() |
|
362 |
if DEBUG then |
|
363 |
love.audio.stop() |
|
364 |
||
365 |
-- create local references to needed variables |
|
366 |
-- because we're about to blow the global scope away |
|
367 |
||
368 |
local initialGlobals = debugger._initialGlobals |
|
369 |
local initialPackages = debugger._initialPackages |
|
370 |
||
371 |
-- reset global scope |
|
372 |
||
373 |
for key, _ in pairs(_G) do |
|
374 |
_G[key] = initialGlobals[key] |
|
375 |
end |
|
376 |
||
377 |
-- reload main file and restart |
|
378 |
||
379 |
for key, _ in pairs(package.loaded) do |
|
380 |
if not initialPackages[key] then |
|
381 |
package.loaded[key] = nil |
|
382 |
end |
|
383 |
end |
|
384 |
||
385 |
require('main') |
|
386 |
love.load() |
|
387 |
end |
|
388 |
end |
|
389 |
end |