21f5607dfbad

First draft of menus article
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Thu, 05 Jan 2017 19:46:43 +0000
parents d9aba755de81
children 0f88cfcdcd44
branches/tags (none)
files content/blog/2016/12/chip8-sound.markdown content/blog/2017/01/chip8-menus.markdown static/media/images/blog/2017/01/chip8-display.png static/media/images/blog/2017/01/chip8-file-select.png static/media/images/blog/2017/01/chip8-menu.png

Changes

--- 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...)
Binary file static/media/images/blog/2017/01/chip8-display.png has changed
Binary file static/media/images/blog/2017/01/chip8-file-select.png has changed
Binary file static/media/images/blog/2017/01/chip8-menu.png has changed