--- a/content/blog/2017/01/chip8-menus.markdown Thu Jan 05 19:46:43 2017 +0000
+++ b/content/blog/2017/01/chip8-menus.markdown Tue Jan 10 16:22:03 2017 +0000
@@ -1,8 +1,8 @@
+++
title = "CHIP-8 in Common Lisp: Menus"
snip = "Let's add some polish."
-date = 2017-01-07T16:40:00Z
-draft = true
+date = 2017-01-10T16:20:00Z
+draft = false
+++
@@ -10,11 +10,11 @@
games, and we've got a rudimentary debugging system in place so we can figure
out where things go wrong.
-Up to now we've been communicating with a running emulator mostly through NREPL
-or SLIME. This is fine for development, but in this post we'll add some
+Up to now we've been communicating with the running emulator mostly through
+NREPL or SLIME. This is fine for development, but in this post we'll add some
much-needed polish in the form of menus. This is the kind of boring work that
-often gets left until the end during game development, so let's just get it over
-and done with and out of the way now.
+often gets left until the end during game development, so let's just get it out
+of the way.
The full series of posts so far:
@@ -24,6 +24,7 @@
4. [CHIP-8 in Common Lisp: Sound](http://stevelosh.com/blog/2016/12/chip8-sound/)
5. [CHIP-8 in Common Lisp: Disassembly](http://stevelosh.com/blog/2017/01/chip8-disassembly/)
6. [CHIP-8 in Common Lisp: Debugging Infrastructure](http://stevelosh.com/blog/2017/01/chip8-debugging-infrastructure/)
+7. [CHIP-8 in Common Lisp: Menus](http://stevelosh.com/blog/2017/01/chip8-menus/)
The full emulator source is on [BitBucket][] and [GitHub][].
@@ -37,12 +38,13 @@
Qtools has some [rudimentary support][qtools-menus] for menus. Unfortunately it
won't quite work with our emulator as-is, so we'll need to shuffle things around
-a bit. When we added the screen to the emulator back in the Graphics post, we
-just created a subclass of `QGLWidget` and passed that along to
+a bit. When we added the screen to the emulator back in the [graphics][] post
+we just created a subclass of `QGLWidget` and passed it along to
`with-main-window`. This works for displaying the screen, but if you try to
`define-menu` on this widget Qtools will signal an error.
[qtools-menus]: https://shinmera.github.io/qtools/#QTOOLS:DEFINE-MENU
+[graphics]: http://stevelosh.com/blog/2016/12/chip8-graphics/
What we need to do is create a `QMainWindow` widget instead, and put our
`QGLWidget` inside of that. Then we can add some menus to the main window and
@@ -82,8 +84,8 @@
(setf (screen-chip screen) chip))
```
-This ensures that when the main window gets closed the screen widget will get
-cleaned up properly.
+This ensures the screen widget will get cleaned up properly when the main window
+is closed.
Now we need to move some of the initialization code that used to be in the
screen widget up into the main window, and also add a bit more to connect
@@ -96,22 +98,22 @@
(q+:focus-proxy main-window) screen))
```
-The `window-title` needs to go on the top-level widget of course, so we can pull
-that out of the screen's initializer. We also set the screen to be the "central
-widget". You can read the [Qt docs][central widget] for the full story, but
-essentially a `QMainWindow` is just a container for other widgets and we need to
-designate one as the primary widget.
+The `window-title` needs to go on the top-level widget, so we can pull that out
+of the screen's initializer. We also set the screen to be the "central widget"
+of the main window. You can read the [Qt docs][central widget] for the full
+story, but essentially a `QMainWindow` is just a container for other widgets and
+we need to designate one as the primary widget.
We'll also want to set the [focus proxy][] of the main window to be the screen,
-because we want the screen to handle all keyboard input just like before.
-Setting the focus proxy tells Qt that whenever the main window gets focused it
-should actually go ahead and focus the screen instead. If we didn't do this,
-then if the main window itself got focused (which can happen when tabbing
-through applications, clicking the title bar, etc) it wouldn't propagate the
-keystrokes down to the screen.
+because we want the screen to handle keyboard input just like before. Setting
+the focus proxy tells Qt that whenever the main window gets focused it should
+actually focus the screen instead. If we didn't do this, then if the main
+window itself got focused (which can happen when tabbing through applications,
+clicking the title bar, etc) it wouldn't propagate the keystrokes down to the
+screen.
-This is all kind of fiddly stuff, but it's the fit and finish that separates
-a toy project from something that actually feels [finished][].
+This is all kind of fiddly stuff, but it's the polish that separates a toy
+project from something that actually feels [finished][].
[focus proxy]: https://doc.qt.io/qt-4.8/qwidget.html#setFocusProxy
[central widget]: https://doc.qt.io/qt-4.8/qmainwindow.html
@@ -137,10 +139,14 @@
(defun run-gui (chip thunk)
(with-main-window
- (window (make-main-window chip)) ; NEW
+ (window (setf *main-window* (make-main-window chip))) ; NEW
(funcall thunk)))
```
+I usually try not to use the return value of a `setf` form because I think it's
+kind of ugly, but it saved an entire `let` here so decided to I break my own
+style rule.
+
## Updating the Screen
The only thing we need to change for the `screen` is its initializer:
@@ -168,7 +174,7 @@
### The File Menu
-Let's start with a simple File menu that will just have two items:
+Let's start with a simple `File` menu that will just have two items:
* Load a ROM
* Quit the emulator
@@ -177,10 +183,8 @@
```lisp
(define-menu (main-window File)
- (:item ("Load ROM..." (ctrl o))
- (load-rom main-window))
- (:item ("Quit" (ctrl q))
- (die)))
+ (:item ("Load ROM..." (ctrl o)) (load-rom main-window))
+ (:item ("Quit" (ctrl q)) (die)))
```
Note that we use the Windows-centric shortcut key names. Qt will handle
@@ -203,12 +207,16 @@
[1f]: https://groups.google.com/forum/message/raw?msg=comp.lang.lisp/9SKZ5YJUmBg/Fj05OZQomzIJ
```lisp
+(defparameter *default-directory*
+ (uiop:native-namestring
+ (asdf:system-source-directory :cl-chip8)))
+
(defun get-rom-path (window)
(let ((path (q+:qfiledialog-get-open-file-name
- window
- "Load ROM"
- (uiop:native-namestring (asdf:system-source-directory :cl-chip8))
- "ROM Files (*.rom);;All Files (*)")))
+ window ; parent widget
+ "Load ROM" ; dialog title
+ *default-directory* ; starting directory
+ "ROM Files (*.rom);;All Files (*)"))) ; filters
(if (string= path "")
nil
path)))
@@ -226,7 +234,7 @@
The filter string is actually parsed by Qt, and by default will prevent the user
from selecting any file that doesn't end in `.rom`. You might want to add a few
more options extensions here if you think people will have named their ROMs
-differently. We also add a second filter that will let the user select any
+differently. We also add a second filter that will let the user select *any*
file, in case they have a ROM with a filename we haven't anticipated. The
result looks like this:
@@ -236,16 +244,16 @@
empty string. We'll check for that and return a more Lispy `nil` from the
function.
-That's it for the file menu. It's basic but trust me: it's a lot nicer to load
-ROMs through a normal dialog than to have to poke at the `chip` in the REPL.
+That's it for the `File` menu. It's basic, but it's a lot nicer to load ROMs
+through a normal dialog than to have to poke at the `chip` in the REPL.
### The Display Menu
-Back in the Graphics post we saw how some ROMs expect the CHIP-8 to wrap sprites
-around the screen when their coordinates get too large, and other ROMs require
-that they *not* wrap. There's no good way to automatically detect this (aside
-from hashing particular ROMs and hard-coding the setting for those) so we'll
-expose this as an option to the user in a Display menu.
+Back in the [graphics][] post we saw how some ROMs expect the CHIP-8 to wrap
+sprites around the screen when their coordinates get too large, and other ROMs
+require that they *not* wrap. There's no good way to automatically detect this
+(aside from hashing particular ROMs and hard-coding the setting for those) so
+we'll expose this as an option to the user in a `Display` menu.
The menu itself is simple — we'll make a submenu that will contain the options
and a helper function to actually do the work:
@@ -282,8 +290,11 @@
### The Sound Menu
Our final menu will allow the user to select what kind of sound the buzzer
-should play, because it would be a shame to let all our work in the Sound post
-go to waste. We'll do it just like the Display menu:
+should play, because it would be a shame to let all our work in the [sound][]
+post go to waste. We'll implement the `Sound` menu just like the `Display`
+menu:
+
+[sound]: http://stevelosh.com/blog/2016/12/chip8-sound/
```lisp
(define-menu (main-window Sound)