Today we finish the tour of the “code base” of my suite of “assistive programs” for players and Judges of Strategic Primer, which I began a couple of months ago with a tour of the “controller” component and continued last month with a look at the “model.” This post wraps our tour up with a look at the “view”—how what the user sees is put together.
As a reminder, this tour is primarily aimed at anyone who might like to contribute code or otherwise help in the development of these programs, but players may find it interesting or useful. I hope to begin posting “user documentation”—descriptions of how to use these programs, in contrast to this series on how they’re built—next month.
And, as before, you can follow along in the source code repository.
Each major “helper application” has its own “view”, which I’ve gathered into a subpackage of the
view package. The map-viewer is even further divided into several sub-subpackages. Since that’s the flagship application of this suite, we’ll start there.
The map viewer is under the
view.map package, which is divided into four parts:
Perhaps the best place to start is with the
ViewerFrame class. This is the window containing everything that the user sees when using the map viewer.
Most of the logic of the
ViewerFrame class is in its constructor; the real “work” is done by the other components it puts together. The constructor sets up the
MapComponent that shows the map and the
DetailPanelNG (from the
view.map.details package) that shows the details of the currently selected tile, sets up scrollbars on the map by putting it in a
MapScrollPanel, and lays them out in the window using a
JSplitPane—that last being a standard Java class I only recently discovered, and the others being classes we’ll get to here shortly. The last thing the
ViewerFrame constructor does is to give the map component the focus, so that the user can start scrolling immediately.
Because I used to have a different implementation of how to display the map, all that any other class needs to know about it (except when it’s being created) is that it implements the
MapGUI marker interface, and that (because it does) it has a copy of the viewer model (which encapsulates the currently-loaded map) that callers can refer to. My first implementation of this used a different component for every tile; the current implementation,
MapComponent, simply draws the visible portion of the map.
And, actually, it doesn’t do much actual drawing. Instead, it clears the drawing area, determines what part of the drawing area actually needs to be redrawn, and hand that, and the “graphics context,” off to a tile draw helper.
TileDrawHelper interface specifies two methods: one to draw a tile with a specified size at the origin (assuming the graphics context has been translated there already), and one to draw a tile at a specified point with a specified size. (Usually one is implemented in terms of the other.) To explain why there are three implementations …
Until the last turn or two, things on the map, other than terrain, units, and fortresses, were represented by ‘events,’ at most one per tile. So when it came time to draw a tile, I used simple geometric shapes to represent whether it had a fortress, unit, and/or ‘event’ on it. There are two
TileDrawHelper implementations that do this, one by creating and caching Shape objects, and the other by doing the drawing directly; I had to do some testing to see which was faster, and left both in the source tree just in case.
When I changed to the notion of ’tile fixtures,’ and made mountains and forests ‘fixtures’ rather than terrain types, I increased the map version, leaving older maps to the older
TileDrawHelper but adding a new
Ver2TileDrawHelper for newer maps. This implementation shows the “top” fixture on a tile, with a background color indicating either the surrounding terrain type or, if there’s a mountain or forest that’s not the top fixture, the presence of that “terrain fixture.”
For any given map, the
TileDrawHelperFactory class creates (if necessary) and returns a suitable implementation to draw its tiles.
MapScrollPanel is a fairly trivial panel. It just embeds the component it’s been passed and calls on the
ScrollListener to set up the scrollbars and the listeners for other ways of scrolling.
ScrollListener class sets up a pair of scroll bars—or changes a pair of scrollbars a caller passes in, but I don’t actually use that variant anywhere—so that they can’t go outside the size of the map, and sets itself up both to adjust the viewer model whenever the scrollbars are moved and to move the scrollbars whenever the viewer model’s notion of the visible area of the map has changed.
MapComponent uses the
ComponentMouseListener class to handle mouse clicks. Right-clicks show a pop-up menu that allows the user to change the terrain type of a tile (the
TerrainChangingMenu), and all clicks select the tile the user clicked on. The
ArrowKeyListener class, which has a
setUpListeners method to hide the complicated details of the initialization sequence from callers, uses the
DirectionSelectionChanger class (which also handles scrolling and zooming using the mouse-wheel) to allow the user to scroll using the arrow keys. And the
MapSizeListener class adjusts the model’s idea of the visible area to match the facts whenever the window is resized.
The remaining classes in this package mostly have to do with the menus. The
SPMenu class is the menu bar on each map-viewer window. Various items in the menus display the
FindDialog window to find a fixture by name, “kind,” or ID number; call the
ZoomListener to adjust the zoom level; and display the
SelectTileDialog window to go directly to a tile whose coordinates the user knows. The
MapFileFilter class doesn’t really belong here, but it’s a filter that makes the “file open” dialogs only show files with the usual extension map files have.
This package contains only a few classes; it was created when showing the ‘details’ of a tile was far more involved and required something like a dozen classes, but the distinction is still a useful one so I’m not going to merge it back into
view.map.main anytime soon.
The main class here is
DetailPanelNG. As the name suggests, it’s a second pass at implementing a ‘details panel.’ It contains three panels: a
TileDetailPanel showing the simple details of the selected tile (its location and terrain type), a panel showing the contents of the tile (more about which momentarily), and a panel showing the ‘key’ or ‘legend’ for the colors representing the various terrain types. (That last is here because it’s collocated with the others in the window’s organization, not because it has anything to do with the current tile.)
To show the contents of the tile, I use a fairly complicated custom
FixtureList class. Most of the logic is, fortunately, handled by the
FixtureListModel we’ve already seen last month, but its constructor sets up drag and drop (allowing the user to copy fixtures between maps) and a listener to let the user delete fixtures using the ‘delete’ or the ‘backspace’ key. And it uses the
FixtureCellRenderer class to draw each list item, with an icon representing it and its description, taking up (ideally) no more and no less vertical space than necessary.
This package is
quite simple. Working from the ground up: each terrain type is represented in the key by a
KeyElementComponent, which is just a component that takes the color it’s supposed to be as a constructor parameter. A
KeyElement combines that with a label saying which terrain type it represents, and some complicated code to make the components not run into each other. And the
KeyPanel creates a
KeyElement for each terrain type that could be used in the current map.
This package contains two fairly new minor drivers,
SubsetFrame. Each displays a colorized version of the output of the corresponding CLI driver in a window.
view.worker package contains the worker-advancement driver. The main class is the
AdvancementFrame. As is the case elsewhere in the view, most of the logic is handled in the model, but I’ll still describe it to some extent here. The
AdvancementFrame is a window with three columns. In the first is a combo box to let the user choose the player whose workers are being dealt with, followed by a list of that player’s units, updated whenever a new player is chosen. When a unit is chosen, a list in the second column displays the members of that unit. The second column also contains a panel that will eventually display the unit’s stats, once the model supports that. If the selected member is a worker, that worker’s Jobs, and the Skills in the selected Job, are displayed in lists in the third column. These last two lists are also accompanied by components to let the user add new Jobs and Skills and increase the number of hours worked in a Skill. And whenever adding hours triggers gaining a new level in a Skill, the
LevelListener inner class logs this to the standard output, so I don’t have to go to some effort to get the list of such gained levels to report to the player.
Because I had some trouble getting the layout to work properly, there’s another inner class,
Resizer, that resizes all the components in the window whenever the window is resized.
Each of the lists in the window is separate custom class, which listens to its model and selects the first item in the list whenever the model reports having finished adding items. They could fairly readily be combined, but that’s not an immediate priority.
The window has a menu, which just contains I/O related items.
This package is mostly quite new code, added since my last development report, and contains the exploration CLI and GUI.
The command-line interface is not new, but still bears going over. The driver (in the controller) calls the
choosePlayer method to get the user to choose a player to run exploration for, then the
chooseUnit to choose one of that player’s units to do the exploring, and then the
moveUntilDone method, which has the user input how many movement points the unit has and then allows him or her to move it until it runs out.
Actual movement is handled by the
move method. Each time through it, the player inputs a number indicating a direction. The method determines what tile is immediately in that direction from the unit’s current location, and tries to move there. If it can, the method creates a list of fixtures on the new tile that should always be noticed and a list of other fixtures that might be noticed, and copies all of the former and one of the latter from the main map to the corresponding tiles on all of the “subordinate” maps (the player “known world” map, in practice), and then it tells the caller how many movement points this cost.
The workflow for the GUI is similar. Everything is contained within an
ExplorationFrame window. Using a
CardLayout, it switches between two panels, the
ExplorerSelectingPanel and the
ExplorationPanel, when the user clicks buttons indicating this.
ExplorerSelectingPanel has two columns; the left column lets the user select a player, and the right column lets him or her select one of that player’s units. A text box asks how many movement points the unit has, and then the user can press a button to switch to the other panel.
ExplorationPanel is rather complicated, visually, but conceptually somewhat simpler than it looks. At the top is a header that says which tile the unit is currently on, and how many movement points are left, and lets the user press a button to go back to the other panel.
Below that header is a 12 by 3 grid of components, representing the currently selected tile and all tiles bordering it. Each tile is represented by a
FixtureList (which you saw above) showing the tile’s contents in the main map, a button—which I’ll get to momentarily—showing the tile on both maps visually, and another
FixtureList showing the tile’s contents in the first “subordinate” map. I chose to use the
FixtureList directly, despite this making other parts of the GUI harder to implement, because that lets the user drag and drop to copy fixtures.
DualTileButton, which I mentioned just now, effectively draws a diagonal line from corner to corner, and draws the main map’s view of the tile (using a
TileDrawHelper, which I mentioned much earlier above) in one half and the first subordinate map’s view of it in the other. It’s also a button; clicking it moves the currently selected unit to the tile it represents, deducts the movement cost from the running total of movement points, and “explores,” copying a set of fixtures that is by default just like what the CLI would do (but can be edited, by selecting a different set in the list to the left of the button) to the subordinate maps, and then refreshing the view. The
ExplorationListListener handles picking a suitable set of fixtures at random (by selecting them in the list) every time a list refreshes, and the
ExplorationClickListener is the class that actually listens for the button presses. (Though it should probably be combined with the button now …)
There’s also a menu that, again, just contains disk I/O items (file open and save, and quit).
This package contains a variety of miscellaneous classes that I thought primarily useful to the view.
AddRemovePanel, which I created for the worker advancement GUI and primarily use there, lets the user add items to and remove them from a list.
AppChooserFrame is the window that the app-chooser driver pops up if the user hasn’t indicated a driver using command-line options.
Applyable interface is for things that can be “applied” and “reverted,” like the (now removed) feature that let users restrict the map viewer’s view to a rectangular area of the map; the
ApplyButtonHandler class makes it easy to associate an
Applyable with a pair of buttons. Similarly,
SaveableOpenable is an interface for things that can be saved to or opened from file.
ConstraintHelper class, which I’d actually written at least once before in another project, is a subclass of the standard Java
GridBagConstraints class that lets me pass the constraints I want into the constructor, enabling a more “functional” style. Similarly,
FilteredFileChooser is a
JFileChooser subclass that just takes a
FileFilter in its constructor, and
MenuItemCreator is a factory class for menu items and hot-keys, since menu items otherwise have to be created in an imperative style, and making hot-keys platform-compatible requires a somewhat verbose “incantation.”
Coordinate is an (x, y) or (width, height) pair, which I needed to add because the
TileDrawHelper interface would otherwise have specified methods with on the order of eight arguments … at which my static analysis tools screamed. Like a
Point, it’s preferred to use the factory method in
PointFactory to create these, since it caches them.
DrawingNumericConstants contains some useful numeric constants (eight, seven-sixteenths, three-quarters, etc.) that the map-version-one
TileDrawHelper implementations need but that static analysis objected to as “magic numbers.”
DriverQuit is a wrapper around
ErrorShower pops up a dialog box to show the user an error; it’s what I use in addition to logging for particularly egregious errors that should never happen, since users won’t always be running the applications from the command line where they’d see the logs.
NullStream is an
OutputStream implementation that does nothing—the Java equivalent of
SystemOut is a wrapper around
System.out, because one of my static analysis tools used to be convinced that
System.out was always
null—and it’s still useful to be able to search for all references in my code to the standard output.
StreamingLabel is a JLabel that provides a way to stream text to it; I use it in the
view.map.misc drivers I mentioned.
UIClosable (so called because there’s a
Closeable interface for streams already, which declares it may throw an exception) is an interface for UI elements that can be closed.
WrapLayout is a
FlowLayout subclass I got from this blog post (its “About Us” page said code from the blog may be used “without restriction”) that wraps components properly. I don’t think I use it anymore now, but I’m leaving it in the tree because I think I’ll probably need it sometime, and not be able to find it.
And that’s the end of it. Next month I’ll start writing user documentation, working my way through the applications in the suite.