# HG changeset patch # User Steve Losh # Date 1483645603 0 # Node ID 21f5607dfbad3f6113c068722b1021c9b6cc788f # Parent d9aba755de81d05fd9b456757c823cbc532428a6 First draft of menus article diff -r d9aba755de81 -r 21f5607dfbad content/blog/2016/12/chip8-sound.markdown --- 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 ``` diff -r d9aba755de81 -r 21f5607dfbad content/blog/2017/01/chip8-menus.markdown --- /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 + +
+ +## 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: + +
+                QMainWindow
+    ╔══════════════════════════════════╗
+    ║ Menu1 │ Menu2 │ ...              ║
+    ║───────┴───────┴──────────────────║
+    ║ ┌──────────────────────────────┐ ║
+    ║ │..............................│ ║
+    ║ │..............................│ ║
+    ║ │..........QGLWidget...........│ ║
+    ║ │.........(the screen).........│ ║
+    ║ │..............................│ ║
+    ║ │..............................│ ║
+    ║ │..............................│ ║
+    ║ └──────────────────────────────┘ ║
+    ╚══════════════════════════════════╝
+
+ +## 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...) diff -r d9aba755de81 -r 21f5607dfbad static/media/images/blog/2017/01/chip8-display.png Binary file static/media/images/blog/2017/01/chip8-display.png has changed diff -r d9aba755de81 -r 21f5607dfbad static/media/images/blog/2017/01/chip8-file-select.png Binary file static/media/images/blog/2017/01/chip8-file-select.png has changed diff -r d9aba755de81 -r 21f5607dfbad static/media/images/blog/2017/01/chip8-menu.png Binary file static/media/images/blog/2017/01/chip8-menu.png has changed