--- a/content/blog/2016/12/chip8-sound.markdown Thu Jan 05 16:41:05 2017 +0000
+++ b/content/blog/2016/12/chip8-sound.markdown Thu Jan 05 19:46:43 2017 +0000
@@ -626,7 +626,7 @@
; NEW
(defun run-gui (chip thunk)
(with-main-window
- (window (make-screen chip))
+ (window (make-screen chip))
(funcall thunk))) ; NEW
```
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/content/blog/2017/01/chip8-menus.markdown Thu Jan 05 19:46:43 2017 +0000
@@ -0,0 +1,311 @@
++++
+title = "CHIP-8 in Common Lisp: Menus"
+snip = "Let's add some polish."
+date = 2017-01-07T16:40:00Z
+draft = true
+
++++
+
+Our [CHIP-8][] emulator in Common Lisp is almost complete. It can play
+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
+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.
+
+The full series of posts so far:
+
+1. [CHIP-8 in Common Lisp: The CPU](http://stevelosh.com/blog/2016/12/chip8-cpu/)
+2. [CHIP-8 in Common Lisp: Graphics](http://stevelosh.com/blog/2016/12/chip8-graphics/)
+3. [CHIP-8 in Common Lisp: Input](http://stevelosh.com/blog/2016/12/chip8-input/)
+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/)
+
+The full emulator source is on [BitBucket][] and [GitHub][].
+
+[CHIP-8]: https://en.wikipedia.org/wiki/CHIP-8
+[BitBucket]: https://bitbucket.org/sjl/cl-chip8
+[GitHub]: https://github.com/sjl/cl-chip8
+
+<div id="toc"></div>
+
+## Architecture
+
+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
+`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
+
+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
+everything should work great. It will end up being structured like this:
+
+<pre class="lineart">
+ QMainWindow
+ ╔══════════════════════════════════╗
+ ║ Menu1 │ Menu2 │ ... ║
+ ║───────┴───────┴──────────────────║
+ ║ ┌──────────────────────────────┐ ║
+ ║ │..............................│ ║
+ ║ │..............................│ ║
+ ║ │..........QGLWidget...........│ ║
+ ║ │.........(the screen).........│ ║
+ ║ │..............................│ ║
+ ║ │..............................│ ║
+ ║ │..............................│ ║
+ ║ └──────────────────────────────┘ ║
+ ╚══════════════════════════════════╝
+</pre>
+
+## Adding a Main Window
+
+We'll start by creating the `QMainWindow` widget. It will just have a single
+slot to keep track of the `chip` struct it's displaying:
+
+```lisp
+(define-widget main-window (QMainWindow)
+ ((chip :accessor main-chip :initarg :chip)))
+```
+
+Next we'll define our screen widget to be a *subwidget* of the main window:
+
+```lisp
+(define-subwidget (main-window screen) (make-instance 'screen)
+ (setf (screen-chip screen) chip))
+```
+
+This ensures that when the main window gets closed the screen widget will get
+cleaned up properly.
+
+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
+everything properly:
+
+```lisp
+(define-initializer (main-window main-setup)
+ (setf (q+:window-title main-window) "cl-chip8"
+ (q+:central-widget main-window) screen
+ (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.
+
+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.
+
+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][].
+
+[focus proxy]: https://doc.qt.io/qt-4.8/qwidget.html#setFocusProxy
+[central widget]: https://doc.qt.io/qt-4.8/qmainwindow.html
+[finished]: http://makegames.tumblr.com/post/1136623767/finishing-a-game
+
+We'll also want to change our `die` function to close this main window, not just
+the screen. I'm going to cheat just a little bit and use a global variable to
+hold the currently-running main window for easy access:
+
+```lisp
+(defparameter *main-window* nil) ; will get set later
+
+(defun die ()
+ (setf (chip8::chip-running (main-chip *main-window*)) nil)
+ (q+:close *main-window*))
+```
+
+Finally, `run-gui` will need to start up a `main-window` instead of a screen:
+
+```lisp
+(defun make-main-window (chip)
+ (make-instance 'main-window :chip chip))
+
+(defun run-gui (chip thunk)
+ (with-main-window
+ (window (make-main-window chip)) ; NEW
+ (funcall thunk)))
+```
+
+## Updating the Screen
+
+The only thing we need to change for the `screen` is its initializer:
+
+```lisp
+(define-initializer (screen screen-setup)
+ (setf (q+:focus-policy screen) (q+:qt.strong-focus)
+ (q+:fixed-size screen) (values *width* *height*)))
+```
+
+We've removed the useless `window-title` that used to be here. Instead we set
+the focus policy to the screen to accept all kinds of focus. If you *don't* set
+this the widget will never be able to get focus (and thus receive keyboard
+events). We also leave in the size setting. The `QMainWindow` seems to pick up
+this size and scale itself appropriately, which is nice.
+
+That's it for the architectural changes. All the screen's drawing and
+input-handling code can remain unchanged.
+
+## Adding Menus
+
+We'll add a couple of menus to make it a bit easier to use the emulator without
+falling back to the Lisp REPL. There's a lot of things we could add, so I'll
+just cover a couple basic options.
+
+### The File Menu
+
+Let's start with a simple File menu that will just have two items:
+
+* Load a ROM
+* Quit the emulator
+
+The menu definition is pretty straightforward:
+
+```lisp
+(define-menu (main-window File)
+ (: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
+translating those to Mac-friendly versions when running on OS X.
+
+We'll hide the details of loading the ROM in a `load-rom` helper function:
+
+```lisp
+(defun load-rom (main-window)
+ (let ((rom (get-rom-path main-window)))
+ (when rom
+ (chip8::load-rom (main-chip main-window) rom))))
+```
+
+`load-rom` just gets the path to load and loads it into the `chip` struct,
+assuming it's not `nil`. Once again we use a helper function to hide the
+details of getting the path to the ROM, because I'm a firm believer in [one
+function to a function][1f]:
+
+[1f]: https://groups.google.com/forum/message/raw?msg=comp.lang.lisp/9SKZ5YJUmBg/Fj05OZQomzIJ
+
+```lisp
+(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 (*)")))
+ (if (string= path "")
+ nil
+ path)))
+```
+
+Qt has a nice static method `QFileDialog.getOpenFileName` that we can use to do
+the heavy lifting. It takes a parent widget (our main window), a title,
+a directory to start in, and a file filter string.
+
+I've used some handy ASDF and UIOP functions to tell the file dialog to start in
+the directory where the emulator's code is located, because that's where I store
+my own ROMs. Another option would be to have it start in the user's home
+directory, or to make the location configurable.
+
+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
+file, in case they have a ROM with a filename we haven't anticipated. The
+result looks like this:
+
+[![Screenshot of the file selection dialog](/media/images/blog/2017/01/chip8-file-select.png)](/media/images/blog/2017/01/chip8-file-select.png)
+
+Note that if the user cancels out of the file selection dialog Qt will return an
+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.
+
+### 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.
+
+The menu itself is simple — we'll make a submenu that will contain the options
+and a helper function to actually do the work:
+
+```lisp
+(define-menu (main-window Display)
+ (:menu "Screen Wrapping"
+ (:item "On" (set-screen-wrapping main-window t))
+ (:item "Off" (set-screen-wrapping main-window nil))))
+
+(defun set-screen-wrapping (main-window enabled)
+ (setf (chip8::chip-screen-wrapping-enabled (main-chip main-window))
+ enabled))
+```
+Now we've got a simple little menu for turning screen wrapping off and on:
+
+[![Screenshot of the display menu](/media/images/blog/2017/01/chip8-display.png)](/media/images/blog/2017/01/chip8-display.png)
+
+You might also want to reset the emulator automatically whenever this option is
+changed, because toggle screen wrapping as the emulator is running can
+produce... *interesting* results. But I like breaking games in fun ways, so
+I left it as-is.
+
+In a perfect world these options wouldn't be vanilla `:item`s but would instead
+be part of a [`QActionGroup`][action-group]. This would tell Qt to treat these
+items as a group, put a checkmark next to the currently-selected one, and so on.
+Unfortunately Qtools doesn't have a menu content type for action groups and the
+thought of implementing [something like this][menu-content] makes me nauseas, so
+I'm satisfied with the `:item` kludge.
+
+[action-group]: https://doc.qt.io/qt-4.8/qactiongroup.html
+[menu-content]: https://github.com/Shinmera/qtools/blob/23e3e44/widget-menu.lisp#L67-L86
+
+### 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:
+
+```lisp
+(define-menu (main-window Sound)
+ (:menu "Sound Type"
+ (:item "Sine" (set-sound-type main-window :sine))
+ (:item "Square" (set-sound-type main-window :square))
+ (:item "Sawtooth" (set-sound-type main-window :sawtooth))
+ (:item "Triangle" (set-sound-type main-window :triangle))))
+
+(defun set-sound-type (main-window type)
+ (setf (chip8::chip-sound-type (main-chip main-window)) type))
+```
+
+## Results
+
+And with that we've got a basic menu system for the emulator:
+
+[![Screenshot of the full menu](/media/images/blog/2017/01/chip8-menu.png)](/media/images/blog/2017/01/chip8-menu.png)
+
+## Future
+
+We're nearing the end of the series. The next post will be about adding
+a graphical debugging interface, and will probably be the last in the series.
+
+(Unless I get ambitious and try making a curses-based ASCII UI...)