diff --git a/rsrc/media/stage/tools/editor/toolbar_icons.png b/rsrc/media/stage/tools/editor/toolbar_icons.png new file mode 100644 index 000000000..6c8a0500a Binary files /dev/null and b/rsrc/media/stage/tools/editor/toolbar_icons.png differ diff --git a/src/java/com/threerings/stage/tools/editor/DirectionButton.java b/src/java/com/threerings/stage/tools/editor/DirectionButton.java new file mode 100644 index 000000000..42ffd7dd6 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/DirectionButton.java @@ -0,0 +1,192 @@ +// +// $Id: DirectionButton.java 5784 2003-01-08 04:17:40Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.AbstractButton; +import javax.swing.DefaultButtonModel; + +import com.threerings.media.image.ColorUtil; + +import com.threerings.util.DirectionCodes; + +/** + * A button that allows for selection from the compass directions. + */ +public class DirectionButton extends AbstractButton +{ + /** + * Construct a 41x41 DirectionButton. + */ + public DirectionButton () + { + this(41); + } + + /** + * Construct a DirectionButton with the specified preferred diameter. + */ + public DirectionButton (int preferredDiameter) + { + _prefdia = preferredDiameter; + configSize(_prefdia); + + setModel(new DefaultButtonModel()); + + addMouseListener(new MouseAdapter() { + public void mousePressed (MouseEvent event) + { + if (isEnabled()) { + _armed = getDirection(event.getX(), event.getY()); + repaint(); + } + } + + public void mouseReleased (MouseEvent event) + { + if ((_armed != -1) && + isEnabled() && + (_armed == getDirection(event.getX(), event.getY())) && + (_direction != _armed)) { + + _direction = _armed; + fireStateChanged(); + } + _armed = -1; + repaint(); + } + + + }); + } + + /** + * Paint this component and the selected direction, + * dimmed if we're inactive. + */ + public void paintComponent (Graphics g) + { + super.paintComponent(g); + + // draw the selected direction circle + g.setColor(isEnabled() ? Color.red + : ColorUtil.blend(Color.red, getBackground())); + g.fillOval(_coords[_direction][0], _coords[_direction][1], + _cdia, _cdia); + + if (_armed != -1) { + g.setColor(ColorUtil.blend(Color.black, getBackground())); + g.fillOval(_coords[_armed][0], _coords[_armed][1], + _cdia, _cdia); + } + + // draw a circle around the various direction circles + g.setColor(isEnabled() ? Color.black + : ColorUtil.blend(Color.black, getBackground())); + g.drawOval(0, 0, _dia, _dia); + + // draw each direction circle + for (int ii=DirectionCodes.SOUTHWEST; + ii < DirectionCodes.DIRECTION_COUNT; + ii++) { + + g.drawOval(_coords[ii][0], _coords[ii][1], _cdia, _cdia); + } + } + + /** + * Get the direction that is currently selected. + */ + public int getDirection () + { + return _direction; + } + + /** + * Set the currently displayed direction + */ + public void setDirection (int direction) + { + if (direction != _direction) { + _direction = direction; + fireStateChanged(); + repaint(); + } + } + + /** + * Get the direction specified by the mouse coordinates + */ + protected int getDirection (int x, int y) + { + for (int ii=0; ii < DirectionCodes.DIRECTION_COUNT; ii++) { + if ((x > _coords[ii][0]) && (x < (_coords[ii][0] + _cdia)) && + (y > _coords[ii][1]) && (y < (_coords[ii][1] + _cdia))) { + return ii; + } + } + return -1; + } + + // documentation inherited + public void setSize (Dimension d) + { + super.setSize(d); + + configSize(Math.min(d.width, d.height)); + } + + /** + * Reconfigure the way we look for a new size + */ + protected void configSize (int diameter) + { + // recompute our sizes + _dia = diameter - 1; + _cdia = _dia / 4; + + int mid = (_dia - _cdia) / 2; + + int num = DirectionCodes.DIRECTION_COUNT; // oh we all know it's 8. + + for (int ii=0; ii < num; ii++) { + double rads = Math.PI * 2 * ii / num; + + // 0 radians specifies EAST, so we offset + int dir = (ii + DirectionCodes.EAST) % num; + + _coords[dir][0] = mid + ((int) (mid * Math.cos(rads))); + _coords[dir][1] = mid + ((int) (mid * Math.sin(rads))); + } + } + + // documentation inherited + public Dimension getPreferredSize () + { + return new Dimension(_prefdia, _prefdia); + } + + /** The current direction displayed by the button. */ + protected int _direction = DirectionCodes.SOUTH; + + // if we're "armed" for a possible state change, which direction is armed? + // (-1 for unarmed) + protected int _armed = -1; + + /** Diameter of the drawn enclosing circle. */ + protected int _dia; + + /** Diameter of selection circles. */ + protected int _cdia; + + /** Coordinates of each of the selection circles. */ + protected int[][] _coords = new int[DirectionCodes.DIRECTION_COUNT][2]; + + /** Our preferred diameter. */ + protected int _prefdia; +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorApp.java b/src/java/com/threerings/stage/tools/editor/EditorApp.java new file mode 100644 index 000000000..cde1991dc --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorApp.java @@ -0,0 +1,356 @@ +// +// $Id: EditorApp.java 19661 2005-03-09 02:40:29Z andrzej $ + +package com.threerings.stage.tools.editor; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.DisplayMode; +import java.awt.EventQueue; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JProgressBar; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.List; + +import com.samskivert.swing.util.SwingUtil; +import com.samskivert.util.DebugChords; +import com.samskivert.util.RuntimeAdjust; +import com.samskivert.util.StringUtil; + +import com.threerings.util.KeyDispatcher; +import com.threerings.util.KeyboardManager; +import com.threerings.util.MessageBundle; +import com.threerings.util.MessageManager; + +import com.threerings.resource.ResourceManager; + +import com.threerings.media.FrameManager; +import com.threerings.media.IconManager; +import com.threerings.media.image.ColorPository; +import com.threerings.media.image.ImageManager; +import com.threerings.media.sound.SoundManager; +import com.threerings.media.util.ModeUtil; + +import com.threerings.media.tile.TileSetRepository; +import com.threerings.media.tile.bundle.BundledTileSetRepository; + +import com.threerings.cast.ComponentRepository; +import com.threerings.cast.bundle.BundledComponentRepository; +import com.threerings.miso.tile.MisoTileManager; + +import com.threerings.stage.data.StageSceneModel; +import com.threerings.stage.tools.editor.util.EditorContext; + +/** + * A scene editor application that provides facilities for viewing, + * editing, and saving the scene templates that comprise a game. + */ +public class EditorApp implements Runnable +{ + /** + * Construct and initialize the EditorApp object. + */ + public EditorApp (final String target) + throws IOException + { + // we need to use heavy-weight popup menus so that they can overly + // our non-lightweight Miso view + JPopupMenu.setDefaultLightWeightPopupEnabled(false); + + // create and size the main application frame + _frame = new EditorFrame(); + + // create our frame manager + _framemgr = FrameManager.newInstance(_frame); + + // create our myriad managers, repositories, etc. + _rmgr = new ResourceManager("rsrc"); + + // build up a simple ui for displaying progress + JPanel progressPanel = new JPanel(new BorderLayout()); + final JLabel progressLabel = new JLabel(); + final JProgressBar progress = new JProgressBar(0,100); + final JPanel spacer = new JPanel(); + + progressPanel.add(progressLabel, BorderLayout.CENTER); + progressPanel.add(progress, BorderLayout.SOUTH); + progressPanel.setPreferredSize(new Dimension(300,80)); + spacer.add(progressPanel); + + _frame.getContentPane().add(spacer); + + final EditorFrame frameRef = _frame; + + ResourceManager.InitObserver obs = new ResourceManager.InitObserver() { + public void progress (int percent, long remaining) { + String msg = "Unpacking..."; + if (remaining >= 0) { + msg += " " + remaining + " seconds remaining."; + } + progressLabel.setText(msg); + progress.setValue(percent); + + if (percent >= 100) { + frameRef.getContentPane().remove(spacer); + EditorApp.this.finishInit(target); + } + } + + public void initializationFailed (Exception e) { + Log.warning("Failed unpacking bundles [e=" + e + "]."); + Log.logStackTrace(e); + } + }; + // we want our methods called on the AWT thread + obs = new ResourceManager.AWTInitObserver(obs); + _rmgr.initBundles(null, "config/resource/editor.properties", obs); + } + + public void finishInit (String target) + { + _msgmgr = new MessageManager("rsrc.i18n"); + _imgr = new ImageManager(_rmgr, _frame); + _tilemgr = new MisoTileManager(_rmgr, _imgr); + + try { + _tsrepo = new BundledTileSetRepository(_rmgr, _imgr, "tilesets"); + _tilemgr.setTileSetRepository(_tsrepo); + _iconmgr = new IconManager(_tilemgr, ICON_MANAGER_CONFIG); + _crepo = new BundledComponentRepository( + _rmgr, _imgr, "components"); + } catch (IOException e) { + Log.warning("Exception loading tilesets and and icon manager " + + "[Exception=" + e + "]."); + return; + } + + _iconmgr.setSource("general"); + _colpos = ColorPository.loadColorPository(_rmgr); + _soundmgr = new SoundManager(_rmgr, null, null); + _kbdmgr = new KeyboardManager(); + _keydisp = new KeyDispatcher(_frame); + + _ctx = new EditorContextImpl(); + + // initialize the frame with the now-prepared context + _frame.init(_ctx, target); + + // wire up our runtime adjustment editor + DebugChords.activate(); + + // if we have a target file, load it up + if (target != null) { + _frame.openScene(target); + } + } + + /** + * Given a subdirectory name (that should correspond to the calling + * service), returns a file path that can be used to store local data. + */ + public static String localDataDir (String subdir) + { + String appdir = System.getProperty("appdir"); + if (StringUtil.blank(appdir)) { + appdir = ".narya-editor"; + String home = System.getProperty("user.home"); + if (!StringUtil.blank(home)) { + appdir = home + File.separator + appdir; + } + } + return appdir + File.separator + subdir; + } + + /** + * Run the application. + */ + public void run () + { + // enter full-screen exclusive mode if available and if we have + // the right display mode + GraphicsEnvironment env = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice gd = env.getDefaultScreenDevice(); + DisplayMode pmode = null; + try { + DisplayMode cmode = gd.getDisplayMode(); + pmode = ModeUtil.getDisplayMode( + gd, cmode.getWidth(), cmode.getHeight(), 16, 15); + } catch (Throwable t) { + // Win98 seems to choke on it's own vomit when we attempt to + // enumerate the available display modes; yay! + Log.warning("Failed to probe display mode."); + Log.logStackTrace(t); + } + + if (_viewFullScreen.getValue() && gd.isFullScreenSupported() && + pmode != null) { + Log.info("Switching to screen mode " + + "[mode=" + ModeUtil.toString(pmode) + "]."); + // set the frame to undecorated, full-screen + _frame.setUndecorated(true); + gd.setFullScreenWindow(_frame); + // switch to our happy custom display mode + gd.setDisplayMode(pmode); + // lay the frame out properly (we can't do this before making + // it full screen because packing causes the window to become + // displayable which apparently prevents the window from + // subsequently being made a full-screen window) + _frame.pack(); + + } else { + _frame.setSize(1024, 768); + SwingUtil.centerWindow(_frame); + _frame.setVisible(true); + } + _framemgr.start(); + } + + /** + * Instantiate the application object and start it running. + */ + public static void main (String[] args) + { + String target = (args.length > 0) ? args[0] : null; + + if (System.getProperty("no_log_redir") != null) { + Log.info("Logging to console only."); + + } else { + String dlog = localDataDir("editor.log"); + try { + PrintStream logOut = new PrintStream( + new BufferedOutputStream(new FileOutputStream(dlog))); + System.setOut(logOut); + System.setErr(logOut); + Log.info("Opened debug log '" + dlog + "'."); + + } catch (IOException ioe) { + Log.warning("Failed to open debug log [path=" + dlog + + ", error=" + ioe + "]."); + } + } + + try { + EditorApp app = new EditorApp(target); + // start up the UI on the AWT thread to avoid deadlocks + EventQueue.invokeLater(app); + + } catch (IOException ioe) { + Log.warning("Unable to initialize editor."); + Log.logStackTrace(ioe); + } + } + + /** + * The implementation of the EditorContext interface that provides + * handles to the config and manager objects that offer commonly + * used services. + */ + protected class EditorContextImpl implements EditorContext + { + public MisoTileManager getTileManager () { + return _tilemgr; + } + + public FrameManager getFrameManager () { + return _framemgr; + } + + public ResourceManager getResourceManager () { + return _rmgr; + } + + public ImageManager getImageManager () { + return _imgr; + } + + public MessageManager getMessageManager () { + return _msgmgr; + } + + public IconManager getIconManager () { + return _iconmgr; + } + + public SoundManager getSoundManager() { + return _soundmgr; + } + + public KeyboardManager getKeyboardManager() { + return _kbdmgr; + } + + public ComponentRepository getComponentRepository () { + return _crepo; + } + + public KeyDispatcher getKeyDispatcher () { + return _keydisp; + } + + public String xlate (String message) { + return xlate("stage.editor", message); + } + + public String xlate (String bundle, String message) { + MessageBundle mbundle = _msgmgr.getBundle(bundle); + if (mbundle == null) { + Log.warning("Requested to translate message with " + + "non-existent bundle [bundle=" + bundle + + ", message=" + message + "]."); + return message; + } else { + return mbundle.xlate(message); + } + } + + public TileSetRepository getTileSetRepository () { + return _tsrepo; + } + + public ColorPository getColorPository () { + return _colpos; + } + + public void enumerateSceneTypes (List types) { + types.add(StageSceneModel.WORLD); + } + } + + protected EditorContext _ctx; + protected EditorFrame _frame; + protected FrameManager _framemgr; + + protected ResourceManager _rmgr; + protected ImageManager _imgr; + protected MisoTileManager _tilemgr; + protected TileSetRepository _tsrepo; + protected ColorPository _colpos; + protected MessageManager _msgmgr; + protected IconManager _iconmgr; + protected SoundManager _soundmgr; + protected KeyboardManager _kbdmgr; + protected BundledComponentRepository _crepo; + protected KeyDispatcher _keydisp; + + /** The path to the config file for the icon manager. */ + protected static final String ICON_MANAGER_CONFIG = + "rsrc/config/stage/iconmgr.properties"; + + /** A debug hook that toggles debug rendering of traversable tiles. */ + protected static RuntimeAdjust.BooleanAdjust _viewFullScreen = + new RuntimeAdjust.BooleanAdjust( + "Toggles whether or not the scene editor uses full screen mode.", + "stage.editor.full_screen", EditorConfig.config, false); +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorConfig.java b/src/java/com/threerings/stage/tools/editor/EditorConfig.java new file mode 100644 index 000000000..44e06dbf0 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorConfig.java @@ -0,0 +1,34 @@ +// +// $Id: EditorConfig.java 1352 2002-03-28 23:55:21Z ray $ + +package com.threerings.stage.tools.editor; + +import com.samskivert.util.Config; + +/** + * Provides access to configuration data for the editor. + */ +public class EditorConfig +{ + /** Provides access to config data for this package. */ + public static Config config = new Config("rsrc/config/stage/tools/editor"); + + /** + * Accessor method for getting the test tile directory. + */ + public static String getTestTileDirectory () + { + return config.getValue(TESTTILE_KEY, TESTTILE_DEF); + } + + /** + * Accessor method for setting the test tile directory. + */ + public static void setTestTileDirectory (String newvalue) + { + config.setValue(TESTTILE_KEY, newvalue); + } + + private static final String TESTTILE_KEY = "testtiledir"; + private static final String TESTTILE_DEF = "."; +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorFrame.java b/src/java/com/threerings/stage/tools/editor/EditorFrame.java new file mode 100644 index 000000000..d31c8a052 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorFrame.java @@ -0,0 +1,552 @@ +// +// $Id: EditorFrame.java 19411 2005-02-19 00:28:49Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.BorderLayout; +import java.awt.EventQueue; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import java.io.File; + +import javax.swing.JComponent; +import javax.swing.JFileChooser; +import javax.swing.JInternalFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollBar; +import javax.swing.KeyStroke; +import javax.swing.filechooser.FileFilter; + +import com.samskivert.swing.GroupLayout; +import com.samskivert.swing.HGroupLayout; +import com.samskivert.swing.util.MenuUtil; +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.media.ManagedJFrame; + +import com.threerings.miso.tile.BaseTileSet; +import com.threerings.miso.util.MisoSceneMetrics; + +import com.threerings.stage.data.StageScene; +import com.threerings.stage.data.StageSceneModel; + +import com.threerings.stage.tools.editor.util.EditorContext; +import com.threerings.stage.tools.editor.util.EditorDialogUtil; +import com.threerings.stage.tools.xml.StageSceneParser; +import com.threerings.stage.tools.xml.StageSceneWriter; + +public class EditorFrame extends ManagedJFrame +{ + public EditorFrame () + { + // treat a closing window as a request to quit + addWindowListener(new WindowAdapter () { + public void windowClosing (WindowEvent e) { + handleQuit(null); + } + }); + + // set our initial window title + setFilePath(null); + } + + public void init (EditorContext ctx, String target) + { + _ctx = ctx; + + // create the editor data model for reference by the panels + _model = new EditorModel(_ctx.getTileManager()); + + // create the file chooser used for saving and loading scenes + if (target == null) { + target = EditorConfig.config.getValue( + "editor.last_dir", System.getProperty("user.dir")); + } + _chooser = (target == null) ? + new JFileChooser() : new JFileChooser(target); + _chooser.setFileFilter(new FileFilter () { + public boolean accept (File f) { + return (f.isDirectory() || f.getName().endsWith(".xml")); + } + public String getDescription () { + return "XML Files"; + } + }); + + // instead of using a popup, we slip the file chooser into the + // main interface (so the editor can run full-screen); thus we + // can't use the standard business and have to add an action + // listen to our file chooser + _chooser.addActionListener(_openListener); + + // set up the menu bar + createMenuBar(); + + // create a top-level panel to manage everything + JPanel top = new JPanel(new BorderLayout()); + + // create a sub-panel to contain the toolbar and scene view + _main = new JPanel(new BorderLayout()); + + // set up the scene view panel with a default scene + _svpanel = new EditorScenePanel(_ctx, this, _model); + _main.add(_svpanel, BorderLayout.CENTER); + + // create a toolbar for action selection and other options + JPanel upper = new JPanel( + new HGroupLayout(GroupLayout.NONE, GroupLayout.LEFT)); + upper.add(new EditorToolBarPanel(_ctx.getTileManager(), _model), + GroupLayout.FIXED); + _sceneInfoPanel = new SceneInfoPanel(_ctx, _model, _svpanel); + upper.add(_sceneInfoPanel, GroupLayout.FIXED); + _main.add(upper, BorderLayout.NORTH); + + // create a couple of scroll bars + JScrollBar horiz = new JScrollBar(JScrollBar.HORIZONTAL); + horiz.setBlockIncrement(500); + horiz.setModel(_svpanel.getHorizModel()); + _main.add(horiz, BorderLayout.SOUTH); + JScrollBar vert = new JScrollBar(JScrollBar.VERTICAL); + vert.setBlockIncrement(500); + vert.setModel(_svpanel.getVertModel()); + _main.add(vert, BorderLayout.EAST); + + // add the scene view and toolbar to the top-level panel + top.add(_main, BorderLayout.CENTER); + + // set up our left sidebar panel + JPanel west = GroupLayout.makeVStretchBox(5); + _tpanel = new TileInfoPanel(_ctx, _model); + west.add(_tpanel); + + JPanel boxPane = GroupLayout.makeHBox(); + boxPane.add(_scrollBox = new EditorScrollBox(_svpanel)); + west.add(boxPane, GroupLayout.FIXED); + + // add the sub-panel to the top panel + top.add(west, BorderLayout.WEST); + + // now add our top-level panel + getContentPane().add(top, BorderLayout.CENTER); + + // observe mouse-wheel events in the scene view panel to allow + // scrolling the wheel to select the tile to be placed + _svpanel.addMouseWheelListener(new MouseWheelListener() { + public void mouseWheelMoved (MouseWheelEvent e) { + if (e.getWheelRotation() < 0) { + _tpanel.selectPreviousTile(); + } else { + _tpanel.selectNextTile(); + } + } + }); + + // finally set a default scene + newScene(); + } + + /** + * Create the menu bar and menu items and add them to the frame. + */ + public void createMenuBar () + { + KeyStroke accel = null; + + // create the "File" menu + JMenu menuFile = new JMenu("File"); + menuFile.setMnemonic(KeyEvent.VK_F); + accel = KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuFile, "New", KeyEvent.VK_N, accel, + this, "handleNew"); + accel = KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuFile, "Open", KeyEvent.VK_O, accel, + this, "handleOpen"); + //addMenuItem(menuFile, "Close", KeyEvent.VK_C); + accel = KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuFile, "Save", KeyEvent.VK_S, accel, + this, "handleSave"); + MenuUtil.addMenuItem(menuFile, "Save As", KeyEvent.VK_A, null, + this, "handleSaveAs"); + accel = KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuFile, "Quit", KeyEvent.VK_Q, accel, + this, "handleQuit"); + + // create the "Edit" menu +// JMenu menuEdit = new JMenu("Edit"); +// MenuUtil.addMenuItem(menuEdit, "Undo", this, "handleUndo"); +// MenuUtil.addMenuItem(menuEdit, "Cut", this, "handleCut"); +// MenuUtil.addMenuItem(menuEdit, "Copy", this, "handleCopy"); +// MenuUtil.addMenuItem(menuEdit, "Paste", this, "handlePaste"); +// MenuUtil.addMenuItem(menuEdit, "Clear", this, "handleClear"); +// MenuUtil.addMenuItem(menuEdit, "Select All", this, "handleSelectAll"); +// menuEdit.setMnemonic(KeyEvent.VK_E); + + // create the "Actions" menu + JMenu menuActions = new JMenu("Actions"); + MenuUtil.addMenuItem(menuActions, "Load (reload) test tiles", this, + "handleTestTiles"); + menuActions.setMnemonic(KeyEvent.VK_A); + + accel = KeyStroke.getKeyStroke(KeyEvent.VK_D, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuActions, "Make default base tile", + KeyEvent.VK_M, accel, this, "handleSetDefBase"); + + accel = KeyStroke.getKeyStroke(KeyEvent.VK_M, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuActions, "Update mini view", + KeyEvent.VK_M, accel, this, "updateMiniView"); + + // create the "Settings" menu + JMenu menuSettings = new JMenu("Settings"); + menuSettings.setMnemonic(KeyEvent.VK_S); + accel = KeyStroke.getKeyStroke( + KeyEvent.VK_SEMICOLON, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuSettings, "Preferences...", KeyEvent.VK_P, + accel, this, "handlePreferences"); + + // create the menu bar + JMenuBar bar = new JMenuBar(); + bar.add(menuFile); + // bar.add(menuEdit); + bar.add(menuActions); + bar.add(menuSettings); + + // add the menu bar to the frame + setJMenuBar(bar); + } + + protected void setScene (StageScene scene) + { + // save off the scene objects + _scene = scene; + + // display the name of the scene + _sceneInfoPanel.setScene(_scene); + + // set up the scene view panel with the new scene + _svpanel.setScene(_scene); + _svpanel.repaint(); + } + + protected void newScene () + { + try { + MisoSceneMetrics metrics = _svpanel.getSceneMetrics(); + StageSceneModel model = StageSceneModel.blankStageSceneModel(); + model.type = StageSceneModel.WORLD; + setScene(new StageScene(model, null)); + + } catch (Exception e) { + Log.warning("Unable to set blank scene."); + Log.logStackTrace(e); + } + } + + /** + * Creates a blank scene and configures the editor to begin editing + * it. Eventually this should make sure the user has a chance to save + * any scene for which editing is currently in progress. + */ + public void handleNew (ActionEvent evt) + { + // clear out the fiel path so that "Save" pops up "Save as" rather + // than overwriting whatever you were editing before + setFilePath(null); + + newScene(); + } + + /** + * Presents the user with an open file dialog and loads the scenes + * from the selected file. + */ + public void handleOpen (ActionEvent evt) + { + _chooser.setDialogType(JFileChooser.OPEN_DIALOG); + _main.remove(_svpanel); + _main.add(_chooser, BorderLayout.CENTER); + SwingUtil.refresh(_main); + } + + /** + * Loads the scene from the specified path. + */ + public void openScene (String path) + { + String errmsg = null; + + // attempt loading and installation of the scene + try { + StageSceneModel model = (StageSceneModel)_parser.parseScene(path); + if (model == null) { + errmsg = "No scene data found"; + } else { + setScene(new StageScene(model, null)); + // keep track of the path for later saving + setFilePath(path); + } + + } catch (Exception e) { + errmsg = "Parse error: " + e; + Log.logStackTrace(e); + } + + if (errmsg != null) { + errmsg = "Unable to load scene from " + path + ":\n" + errmsg; + JOptionPane.showMessageDialog( + this, errmsg, "Load error", JOptionPane.ERROR_MESSAGE); + } + } + + /** + * Save the scenes to the file they were last associated with. + */ + public void handleSave (ActionEvent evt) + { + if (_filepath == null) { + handleSaveAs(evt); + return; + } + + if (!checkSaveOk()) { + return; + } + + try { + // we don't write directly to the file in question, in case + // something craps out during the writing process + File tmpfile = new File(_filepath + ".tmp"); + _writer.writeScene(tmpfile, _scene.getSceneModel()); + + // now that we've successfully written the new file, delete + // the old file + File sfile = new File(_filepath); + if (!sfile.delete()) { + Log.warning("Aiya! Not able to remove " + _filepath + + " so that we can replace it with " + + tmpfile.getPath() + "."); + } + + // now rename the new save file into place + if (!tmpfile.renameTo(sfile)) { + Log.warning("Fork! Not able to rename " + tmpfile.getPath() + + " to " + _filepath + "."); + } + + } catch (Exception e) { + String errmsg = "Unable to save scene to " + _filepath + ":\n" + e; + JOptionPane.showMessageDialog( + this, errmsg, "Save error", JOptionPane.ERROR_MESSAGE); + Log.warning("Error writing scene " + + "[fname=" + _filepath + ", e=" + e + "]."); + Log.logStackTrace(e); + } + } + + /** + * Check to see if the save can proceed, and pop up errors if it + * can't. + */ + protected boolean checkSaveOk () + { + String name = _scene.getName(); + String type = _scene.getType(); + + String err = ""; + if (name.equals("") || name.indexOf("<") != -1 || + name.indexOf(">") != -1) { + err += "Invalid scene name.\n"; + } + + if (type.equals("")) { + err += "No scene type specified.\n"; + } + + if (_svpanel.getEntrance() == null) { + err += "No default entrance portal.\n"; + } + + if ("".equals(err)) { + return true; + } else { + JOptionPane.showMessageDialog( + this, err + "\nPlease fix and try again.", + "Won't save: scene is broken!", + JOptionPane.ERROR_MESSAGE); + return false; + } + } + + /** + * Present the user with a save file dialog and save the scenes to + * the selected file. + */ + public void handleSaveAs (ActionEvent evt) + { + _chooser.setDialogType(JFileChooser.SAVE_DIALOG); + _main.remove(_svpanel); + _main.add(_chooser, BorderLayout.CENTER); + SwingUtil.refresh(_main); + } + + /** + * Keeps our file path around and conveys that information in the + * window title. + */ + protected void setFilePath (String filepath) + { + _filepath = filepath; + setTitle("Narya Scene Editor: " + + (_filepath == null ? "" : _filepath)); + } + + /** + * Handles a request to quit. Presently this just quits, but + * eventually it should give the user the chance to save any edits in + * progress. + */ + public void handleQuit (ActionEvent evt) + { + System.exit(0); + } + + /** + * Handles a request to reload the test tiles. + */ + public void handleTestTiles (ActionEvent evt) + { + // don't instantiate a testLoader until we actually need one + if (_testLoader == null) { + _testLoader = new TestTileLoader(); + } + + _tpanel.insertTestTiles(_testLoader.loadTestTiles()); + } + + /** + * Update the mini view in the scrollbox. + */ + public void updateMiniView (ActionEvent evt) + { + _scrollBox.updateView(); + } + + /** + * Handles a request to open the preferences dialog. + */ + public void handlePreferences (ActionEvent evt) + { + PreferencesDialog pd = new PreferencesDialog(); + EditorDialogUtil.display(this, pd); + } + + /** + * Make the currently selected base tile into the scene's default + * tile. + */ + public void handleSetDefBase (ActionEvent evt) + { + if (_model.getTileSet() instanceof BaseTileSet) { + _svpanel.updateDefaultTileSet(_model.getTileSetId()); + } else { + Log.warning("Not making non-base tileset into default " + + _model.getTileSet() + "."); + } + } + + /** Handles JFileChooser responses when opening files. */ + protected ActionListener _openListener = new ActionListener() { + public void actionPerformed (ActionEvent event) { + String cmd = event.getActionCommand(); + + // restore the scene view + _main.remove(_chooser); + _main.add(_svpanel); + SwingUtil.refresh(_main); + + // load up the scene if they selected one + if (cmd.equals(JFileChooser.APPROVE_SELECTION)) { + File filescene = _chooser.getSelectedFile(); + switch (_chooser.getDialogType()) { + case JFileChooser.OPEN_DIALOG: + openScene(filescene.getPath()); + EditorConfig.config.setValue( + "editor.last_dir", filescene.getParent()); + break; + + case JFileChooser.SAVE_DIALOG: + setFilePath(filescene.getPath()); + handleSave(null); + break; + + default: + Log.warning("Wha? Weird dialog type " + + _chooser.getDialogType() + "."); + break; + } + } + + // oh god the hackery; on linux at least, the focus seems + // to be hosed after we hide the chooser and we can't just + // request to move it somewhere useful because it just + // ignores us; so instead we have to wait for the current + // event queue to flush before we can transfer focus + EventQueue.invokeLater(new Runnable() { + public void run () { + _sceneInfoPanel.requestFocus(); + } + }); + } + }; + + /** The scene currently undergoing edit. */ + protected StageScene _scene; + + /** The file last associated with the current scene. */ + protected String _filepath; + + /** Contains the scene view panel or other fun stuff. */ + protected JPanel _main; + + /** Used for displaying dialogs. */ + protected JInternalFrame _dialog; + + /** The file chooser used for loading and saving scenes. */ + protected JFileChooser _chooser; + + /** The panel that displays the scene view. */ + protected EditorScenePanel _svpanel; + + /** The panel that displays tile info. */ + protected TileInfoPanel _tpanel; + + /** The panel that displays scene info. */ + protected SceneInfoPanel _sceneInfoPanel; + + /** The scrollbox used to display the view position within the scene. */ + protected EditorScrollBox _scrollBox; + + /** The editor data model. */ + protected EditorModel _model; + + /** The editor context. */ + protected EditorContext _ctx; + + /** We use this to load scenes. */ + protected StageSceneParser _parser = new StageSceneParser(); + + /** We use this to save scenes. */ + protected StageSceneWriter _writer = new StageSceneWriter(); + + /** The test tileset loader. */ + protected TestTileLoader _testLoader; +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorModel.java b/src/java/com/threerings/stage/tools/editor/EditorModel.java new file mode 100644 index 000000000..e185b8279 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorModel.java @@ -0,0 +1,262 @@ +// +// $Id: EditorModel.java 17780 2004-11-10 23:00:07Z ray $ + +package com.threerings.stage.tools.editor; + +import java.util.ArrayList; + +import com.threerings.media.tile.Tile; +import com.threerings.media.tile.TileManager; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileUtil; + +import com.threerings.util.DirectionCodes; + +/** + * The EditorModel class provides a holding place for storing, + * modifying and retrieving data that is shared across the Editor + * application and its myriad UI components. + */ +public class EditorModel +{ + /** Action mode constants. */ + public static final int ACTION_PLACE_TILE = 0; + public static final int ACTION_EDIT_TILE = 1; + public static final int ACTION_PLACE_PORTAL = 2; + + /** The number of actions. */ + public static final int NUM_ACTIONS = 3; + + /** Miso layer constants. */ + public static final int BASE_LAYER = 0; + public static final int OBJECT_LAYER = 1; + public static final String[] LAYER_NAMES = { "Base", "Object" }; + + /** Prose descriptions of actions suitable for tool tip display. */ + public static final String[] TIP_ACTIONS = { + "Place/Delete tiles.", "Edit object tiles.", + "Create/Edit/Delete portal." + }; + + /** String translations for action identifiers. */ + public static final String[] CMD_ACTIONS = { + "place_tile", "edit_tile", "place_portal" + }; + + public EditorModel (TileManager tilemgr) + { + _tilemgr = tilemgr; + _tileSetId = _tileIndex = -1; + _lnum = _mode = 0; + } + + /** + * Add an editor model listener. + * + * @param l the listener. + */ + public void addListener (EditorModelListener l) + { + if (!_listeners.contains(l)) { + _listeners.add(l); + } + } + + /** + * Notify all model listeners that the editor model has changed. + */ + protected void notifyListeners (int event) + { + int size = _listeners.size(); + for (int ii = 0; ii < size; ii++) { + ((EditorModelListener)_listeners.get(ii)).modelChanged(event); + } + } + + /** + * Returns whether the currently selected tile is valid. + */ + public boolean isTileValid () + { + return (_tile != null); + } + + /** + * Returns the current editor action mode. + */ + public int getActionMode () + { + return _mode; + } + + /** + * Returns the current scene layer index undergoing edit. + */ + public int getLayerIndex () + { + return _lnum; + } + + /** + * Returns the currently selected tile set. + */ + public TileSet getTileSet () + { + return _tileSet; + } + + /** + * Returns the currently selected tile set id. + */ + public int getTileSetId () + { + return _tileSetId; + } + + /** + * Returns the currently selected tile id within the selected tile + * set. + */ + public int getTileId () + { + return _tileIndex; + } + + /** + * Returns the currently selected tile for placement within the + * scene. + */ + public Tile getTile () + { + return _tile; + } + + /** + * Returns the fully qualified tile id of the currently selected tile. + */ + public int getFQTileId () + { + return _fqTileId; + } + + /** + * Marks the currently selected tile as invalid such that editing + * when no tiles are available can be properly effected. + */ + public void clearTile () + { + _tileSet = null; + _tileSetId = -1; + _tileIndex = -1; + _tile = null; + } + + /** + * Sets the editor action mode. The specified mode should be one + * of CMD_ACTIONS. + */ + public void setActionMode (String cmd) + { + for (int ii = 0; ii < CMD_ACTIONS.length; ii++) { + if (CMD_ACTIONS[ii].equals(cmd)) { + _mode = ii; + notifyListeners(EditorModelListener.ACTION_MODE_CHANGED); + return; + } + } + + Log.warning("Attempt to set to unknown mode [cmd=" + cmd + "]."); + } + + /** + * Sets the scene layer index undergoing edit. + */ + public void setLayerIndex (int lnum) + { + if (lnum != _lnum) { + _lnum = lnum; + notifyListeners(EditorModelListener.LAYER_INDEX_CHANGED); + } + } + + /** + * Sets the selected tile for placement within the scene. + */ + public void setTile (TileSet set, int tileSetId, int tileIndex) + { + _tile = set.getTile(tileIndex); + _tileSet = set; + _tileSetId = tileSetId; + _tileIndex = tileIndex; + _fqTileId = TileUtil.getFQTileId(tileSetId, tileIndex); + notifyListeners(EditorModelListener.TILE_CHANGED); + } + + /** + * Sets the tile id of the tile selected for placement within the + * scene. + */ + public void setTileId (int tileIndex) + { + setTile(_tileSet, _tileSetId, tileIndex); + } + + /** + * Sets the direction in which we should grip objects. + */ + public void setObjectGripDirection (int direction) + { + _objectGrip = direction; + } + + /** + * Gets the direction in which we should grip objects. + */ + public int getObjectGripDirection () + { + return _objectGrip; + } + + /** + * Returns a string representation of the editor model. + */ + public String toString () + { + StringBuffer buf = new StringBuffer(); + buf.append("[set=").append(_tileSet); + buf.append(", tid=").append(_tileIndex); + buf.append(", lnum=").append(_lnum); + buf.append(", tile=").append(_tile); + return buf.append("]").toString(); + } + + /** The currently selected action mode. */ + protected int _mode; + + /** The currently selected tileset. */ + protected TileSet _tileSet; + + /** The currently selected tileset id. */ + protected int _tileSetId; + + /** The currently selected tile id. */ + protected int _tileIndex; + + /** The fully qualified tile id of the currently selected tile. */ + protected int _fqTileId; + + /** The currently selected layer number. */ + protected int _lnum; + + /** The currently selected tile for placement in the scene. */ + protected Tile _tile; + + /** The model listeners. */ + protected ArrayList _listeners = new ArrayList(); + + /** The tile manager. */ + protected TileManager _tilemgr; + + /** Direction (which corner) we grip an object by. */ + protected int _objectGrip = DirectionCodes.SOUTH; +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorModelListener.java b/src/java/com/threerings/stage/tools/editor/EditorModelListener.java new file mode 100644 index 000000000..3fc7cd347 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorModelListener.java @@ -0,0 +1,24 @@ +// +// $Id: EditorModelListener.java 217 2001-11-29 00:29:31Z mdb $ + +package com.threerings.stage.tools.editor; + +/** + * The editor model listener interface should be implemented by + * classes that would like to be notified when the editor model is + * changed. + * + * @see EditorModel + */ +public interface EditorModelListener +{ + /** + * Called by the {@link EditorModel} when the model is changed. + */ + public void modelChanged (int event); + + /** Notification event constants. */ + public static final int ACTION_MODE_CHANGED = 0; + public static final int LAYER_INDEX_CHANGED = 1; + public static final int TILE_CHANGED = 2; +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorScenePanel.java b/src/java/com/threerings/stage/tools/editor/EditorScenePanel.java new file mode 100644 index 000000000..b62757628 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorScenePanel.java @@ -0,0 +1,1143 @@ +// +// $Id: EditorScenePanel.java 20143 2005-03-30 01:12:48Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.Stroke; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.geom.Ellipse2D; + +import java.util.ArrayList; +import java.util.Iterator; + +import javax.swing.BoundedRangeModel; +import javax.swing.DefaultBoundedRangeModel; +import javax.swing.JFrame; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.samskivert.swing.Controller; +import com.samskivert.swing.TGraphics2D; +import com.samskivert.swing.util.SwingUtil; +import com.samskivert.util.StringUtil; +import com.threerings.util.DirectionCodes; +import com.threerings.util.RandomUtil; + +import com.threerings.media.tile.ObjectTile; +import com.threerings.media.tile.Tile; +import com.threerings.media.tile.TileUtil; + +import com.threerings.whirled.spot.tools.EditablePortal; +import com.threerings.whirled.spot.data.Portal; + +import com.threerings.miso.client.ObjectActionHandler; +import com.threerings.miso.client.SceneBlock; +import com.threerings.miso.client.SceneObject; +import com.threerings.miso.data.MisoSceneModel; +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.util.MisoUtil; + +import com.threerings.stage.client.StageScenePanel; +import com.threerings.stage.data.StageMisoSceneModel; + +import com.threerings.stage.data.StageScene; +import com.threerings.stage.tools.editor.util.EditorContext; +import com.threerings.stage.tools.editor.util.EditorDialogUtil; +import com.threerings.stage.tools.editor.util.ExtrasPainter; + +/** + * Displays the scene view and handles UI events on the scene. Various + * actions may be performed on the scene depending on the selected action + * mode, including placing and deleting tiles or locations and creating + * portals. + */ +public class EditorScenePanel extends StageScenePanel + implements EditorModelListener, KeyListener, ChangeListener +{ + /** + * Constructs the editor scene view panel. + */ + public EditorScenePanel (EditorContext ctx, JFrame frame, EditorModel model) + { + super(ctx, new Controller() { + }); + + // keep these around for later + _ctx = ctx; + _frame = frame; + + // save off references to our objects + _emodel = model; + _emodel.addListener(this); + + // listen for alt keys + ctx.getKeyDispatcher().addGlobalKeyListener(this); + + // listen to our range models and scroll our badself + _horizRange.addChangeListener(this); + _vertRange.addChangeListener(this); + } + + /** + * Returns a range model that controls the scrollability of the scene + * in the horizontal direction. + */ + public BoundedRangeModel getHorizModel () + { + return _horizRange; + } + + /** + * Returns a range model that controls the scrollability of the scene + * in the vertical direction. + */ + public BoundedRangeModel getVertModel () + { + return _vertRange; + } + + // documentation inherited from interface + public void stateChanged (ChangeEvent e) + { + setViewLocation(_horizRange.getValue(), _vertRange.getValue()); + _refreshBox = true; + } + + // documentation inherited + public void setSceneModel (MisoSceneModel model) + { + super.setSceneModel(model); + + // compute the "area" in which we'll allow the view to scroll + computeScrollArea(); + } + + /** + * Updates the default tileset in the currently edited scene. + */ + public void updateDefaultTileSet (int tileSetId) + { + _model.setDefaultBaseTileSet(tileSetId); + _blocks.clear(); + rethink(); + } + + // documentation inherited + public void setBounds (int x, int y, int width, int height) + { + super.setBounds(x, y, width, height); + + updateScrollArea(_horizRange.getValue(), _vertRange.getValue()); + } + + /** + * Computes the area in which the view is allowed to scroll. + */ + protected void computeScrollArea () + { + StageMisoSceneModel ysmodel = (StageMisoSceneModel)_model; + _area = null; + for (Iterator iter = ysmodel.getSections(); iter.hasNext(); ) { + StageMisoSceneModel.Section sect = + (StageMisoSceneModel.Section)iter.next(); + Rectangle sbounds = MisoUtil.getFootprintPolygon( + _metrics, sect.x, sect.y, + ysmodel.swidth, ysmodel.sheight).getBounds(); + if (_area == null) { + _area = sbounds; + } else { + _area.add(sbounds); + } + } + + // if we have no blocks, fake something up to start + if (_area == null) { + _area = new Rectangle(-250, -250, 500, 500); + } + + updateScrollArea(_horizRange.getValue(), _vertRange.getValue()); + } + + /** + * Updates our bounded range models to reflect potential changes in + * the viewable area and the scrollable area. + */ + protected void updateScrollArea (int hval, int vval) + { + int hmax = _area.x+_area.width; + hval = Math.min(hval, hmax-_vbounds.width); + _horizRange.setRangeProperties( + hval, _vbounds.width, _area.x, hmax, false); + + int vmax = _area.y+_area.height; + vval = Math.min(vval, vmax-_vbounds.height); + _vertRange.setRangeProperties( + vval, _vbounds.height, _area.y, vmax, false); + +// Log.info("Updated extents area:" + StringUtil.toString(_area) + +// " vb:" + StringUtil.toString(_vbounds) + "."); + // update the dimensions of the scrollbox + + // possibly refresh the dimensions of the box + // and queue a repaint for it + SwingUtil.refresh(_box); + _refreshBox = true; + } + + /** + * Handle placing the currently selected tile at the given screen + * coordinates in the scene. + */ + protected void placeTile (int x, int y) + { + Rectangle drag = clearTileSelectRegion(x, y); + + // sanity check + if (!_emodel.isTileValid()) { + return; + } + + switch (_emodel.getLayerIndex()) { + case EditorModel.BASE_LAYER: + updateBaseTiles(x, y, drag, _emodel.getFQTileId(), + _emodel.getTileSetId(), + _emodel.getTileSet().getTileCount()); + break; + + case EditorModel.OBJECT_LAYER: + addObject((ObjectTile)_emodel.getTile(), + _emodel.getFQTileId(), x, y); + break; + } + + // potentially update our scrollable area + computeScrollArea(); + } + + /** + * Handle deleting the tile at the given screen coordinates from + * the scene. + */ + protected void deleteTile (int x, int y) + { + Rectangle drag = clearTileSelectRegion(x, y); + Log.info("Deleting " + drag); + + switch (_emodel.getLayerIndex()) { + case EditorModel.BASE_LAYER: + updateBaseTiles(x, y, drag, 0, 0, 1); + break; + + case EditorModel.OBJECT_LAYER: + if (drag != null) { + // locate any object that intersects this rectangle + ArrayList hits = new ArrayList(); + for (Iterator iter = _vizobjs.iterator(); iter.hasNext(); ) { + SceneObject scobj = (SceneObject)iter.next(); + if (scobj.objectFootprintOverlaps(drag)) { + hits.add(scobj); + } + } + // and delete 'em + for (int ii = 0; ii < hits.size(); ii++) { + deleteObject((SceneObject)hits.get(ii)); + } + + } else { + // delete the object tile over which the mouse is hovering + if (_hobject instanceof SceneObject) { + deleteObject((SceneObject)_hobject); + } + } + break; + } + } + + /** + * Used to place or delete base tiles. + */ + protected void updateBaseTiles (int x, int y, Rectangle drag, + int fqTileId, int tileSetId, int tileCount) + { + if (drag == null) { + setBaseTile(fqTileId, x, y); + } else { + setBaseTiles(drag, tileSetId, tileCount); + } + } + + /** + * Handle editing the tile at the given screen coordinates from the + * scene. If the tile is not an object tile, we don't do anything. + */ + protected void editTile (int x, int y) + { + // bail if we're not hovering over a scene object + if (_hobject == null || !(_hobject instanceof SceneObject)) { + return; + } + + // create our object editor dialog if we haven't yet + if (_objEditor == null) { + _objEditor = new ObjectEditorDialog(_ctx, this); + } + + // prepare and display our object editor dialog + _eobject = (SceneObject)_hobject; + _objEditor.prepare(_eobject); + EditorDialogUtil.display(_frame, _objEditor); + } + + /** + * Called by the {@link ObjectEditorPanel} when it is dismissed. + */ + protected void objectEditorDismissed () + { + recomputeVisible(); + _model.updateObject(_eobject.info); + _eobject = null; + } + + /** + * Pop up the portal dialog for the specified location. + */ + protected void editPortal (EditablePortal portal) + { + // create our portal dialog if we haven't yet + if (_dialogPortal == null) { + _dialogPortal = new PortalDialog(); + } + + // pass location information on to the dialog + _dialogPortal.prepare((StageScene)_scene, portal); + + // allow the user to edit the info + EditorDialogUtil.display(_frame, _dialogPortal); + + // this gets called when a portal is added, so go ahead and + // recompute them little buggers + recomputePortals(); + recomputeVisible(); + } + + // documentation inherited + public void modelChanged (int event) + { + switch (event) { + case ACTION_MODE_CHANGED: + switch (_emodel.getActionMode()) { + case EditorModel.ACTION_PLACE_TILE: + enableCoordHighlighting(false); + if (_emodel.isTileValid()) { + setPlacingTile(_emodel.getTile()); + } + break; + + case EditorModel.ACTION_EDIT_TILE: + setPlacingTile(null); + enableCoordHighlighting(false); + break; + + case EditorModel.ACTION_PLACE_PORTAL: + setPlacingTile(null); + enableCoordHighlighting(true); + break; + } + break; + + case TILE_CHANGED: + if (_emodel.isTileValid() && + _emodel.getActionMode() == EditorModel.ACTION_PLACE_TILE) { + setPlacingTile(_emodel.getTile()); + } + break; + } + + repaint(); + } + + // documentation inherited from interface + public void mousePressed (MouseEvent event) + { + int mx = event.getX(), my = event.getY(); + switch (_emodel.getActionMode()) { + case EditorModel.ACTION_PLACE_TILE: +// if (_emodel.getLayerIndex() == EditorModel.BASE_LAYER) { + setTileSelectRegion(getTileCoords(mx, my)); +// } + break; + + case EditorModel.ACTION_PLACE_PORTAL: + Point fcoords = getFullCoords(mx, my); + // mouse button three is delete + if (event.getButton() == MouseEvent.BUTTON3) { + deletePortal(fcoords.x, fcoords.y); + } else { + // if they clicked on an existing portal... + EditablePortal portal = (EditablePortal) + getPortal(fcoords.x, fcoords.y); + if (portal != null) { + // ...edit it... + editPortal(portal); + } else { + // ...otherwise create a new one + new PortalTool().init(this, mx, my); + } + } + break; + + default: + super.mousePressed(event); + break; + } + } + + // documentation inherited + protected boolean handleMousePressed (Object hobject, MouseEvent event) + { + // don't do the standard cluster and location stuff here + return false; + } + + // documentation inherited + public void mouseReleased (MouseEvent e) + { + super.mouseReleased(e); + + Point tc = getTileCoords(e.getX(), e.getY()); + switch (_emodel.getActionMode()) { + case EditorModel.ACTION_PLACE_TILE: + switch (e.getButton()) { + case MouseEvent.BUTTON1: + placeTile(tc.x, tc.y); + _refreshBox = true; + break; + + case MouseEvent.BUTTON2: + editTile(tc.x, tc.y); + break; + + case MouseEvent.BUTTON3: + deleteTile(tc.x, tc.y); + _refreshBox = true; + break; + } + break; + + case EditorModel.ACTION_EDIT_TILE: + editTile(tc.x, tc.y); + break; + + case EditorModel.ACTION_PLACE_PORTAL: + // nothing to do here; the portal tool handles all + break; + } + + repaint(); + } + + // documentation inherited + public void mouseMoved (MouseEvent e) + { + super.mouseMoved(e); + int x = e.getX(), y = e.getY(); + boolean repaint = false; + + // update the potential tile placement + if (_ptile != null) { + boolean changed; + + if (_ptile instanceof ObjectTile) { + if (changed = updateObjectTileCoords( + x, y, _ppos, (ObjectTile)_ptile)) { + _pscobj.relocateObject(_metrics, _ppos.x, _ppos.y); + } + } else { + changed = updateTileCoords(x, y, _ppos); + } + + if (changed) { + _validPlacement = + isTilePlacementValid(_ppos.x, _ppos.y, _ptile); + repaint = true; + } + } + + // update the highlighted portal's fine coordinates + if (_coordHighlighting) { + repaint = (updateCoordPos(x, y, _hfull) || repaint); + } + + // TODO: dirty things with a finer grain + if (repaint) { + repaint(); + } + } + + // documentation inherited + public void mouseDragged (MouseEvent e) + { + super.mouseDragged(e); + // do the same thing as when we move + mouseMoved(e); + } + + // documentation inherited + public void mouseExited (MouseEvent e) + { + super.mouseExited(e); + + // remove any highlighted tiles and placing tile + _ppos.setLocation(Integer.MIN_VALUE, 0); + _hfull.setLocation(Integer.MIN_VALUE, 0); + } + + // documentation inherited + public void keyPressed (KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_ALT) { + // enable scene view tooltips + setShowFlags(SHOW_TIPS, true); + } + } + + // documentation inherited + public void keyReleased (KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_ALT) { + // disable scene view tooltips + setShowFlags(SHOW_TIPS, false); + } + } + + // documentation inherited + public void keyTyped (KeyEvent e) + { + // nothing + } + + // documentation inherited + protected void fireObjectAction ( + ObjectActionHandler handler, SceneObject scobj, ActionEvent event) + { + // do nothing in the editor thanksverymuch + } + + /** + * A place for subclasses to react to the hover object changing. + * One of the supplied arguments may be null. + */ + protected void hoverObjectChanged (Object oldHover, Object newHover) + { + super.hoverObjectChanged(oldHover, newHover); + + // we always repaint our objects when the hover changes + if (oldHover instanceof SceneObject) { + SceneObject oldhov = (SceneObject)oldHover; + _remgr.invalidateRegion(oldhov.getObjectFootprint().getBounds()); + } + if (newHover instanceof SceneObject) { + SceneObject newhov = (SceneObject)newHover; + _remgr.invalidateRegion(newhov.getObjectFootprint().getBounds()); + } + } + + /** + * Sets a base tile at the specified position in the scene (in tile + * coordinates). + */ + public void setBaseTile (int fqTileId, int x, int y) + { + if (!_model.setBaseTile(fqTileId, x, y)) { + return; + } + getBlock(x, y).updateBaseTile(fqTileId, x, y); + + // and recompute any surrounding fringe + for (int fx = x-1, xn = x+1; fx <= xn; fx++) { + for (int fy = y-1, yn = y+1; fy <= yn; fy++) { + getBlock(fx, fy).updateFringe(fx, fy); + } + } + } + + /** + * Set a region of tiles to a random selection from the supplied + * tileset. + */ + public void setBaseTiles (Rectangle r, int setId, int tileCount) + { + for (int x = r.x; x < r.x + r.width; x++) { + for (int y = r.y; y < r.y + r.height; y++) { + int index = RandomUtil.getInt(tileCount); + int fqTileId = TileUtil.getFQTileId(setId, index); + setBaseTile(fqTileId, x, y); + } + } + } + + /** + * Sets an object tile at the specified position in the scene (in tile + * coordinates). + */ + public void addObject (ObjectTile tile, int fqTileId, int x, int y) + { + Point p = new Point(x, y); + adjustObjectCoordsAccordingToGrip(p, tile); + + ObjectInfo oinfo = new ObjectInfo(fqTileId, x, y); + + // first attempt to add it to the appropriate scene block; this + // will fail if there's already a copy of the same object at this + // coordinate + if (getBlock(x, y).addObject(oinfo)) { + // create an object info and add it to the scene model + _model.addObject(oinfo); + // recompute our visible object set + recomputeVisible(); + } + } + + /** + * Deletes the object tile at the specified tile coordinates. + */ + public void deleteObject (SceneObject scobj) + { + // remove it from the scene model + if (_model.removeObject(scobj.info)) { + // clear the object out of its block + getBlock(scobj.info.x, scobj.info.y).deleteObject(scobj.info); + } else { + Log.warning("Requested to remove unknown object " + scobj + "."); + } + + // recompute our visible object set + recomputeVisible(); + + // make sure we clear the hover if that's what we're deleting + if (_hobject == scobj) { + _hobject = null; + } + } + + /** + * Sets the tile that is currently being placed. It will not be + * rendered until after a call to {@link #updateTileCoords} on the + * placing tile (which happens automatically when the mouse moves). + */ + public void setPlacingTile (Tile tile) + { + _ptile = tile; + + // if this is an object tile, create a temporary scene object we + // can use to perform calculations with the object while placing + if (_ptile instanceof ObjectTile) { + _pscobj = new SceneObject(this, new ObjectInfo(0, _ppos.x, _ppos.y), + (ObjectTile)tile); + } else { + _pscobj = null; + } + } + + /** + * Sets the start (in tile coords) of a mouse drag when placing + * a rectangular area of base tiles. + */ + public void setTileSelectRegion (Point drag) + { + _drag = drag; + } + + /** + * Clear and return the drag rectangle for selecting a rectangular + * region. + * + * @return null if the drag is the same as the supplied tile + * coordinates, a rectangle containing the selected region if it was + * different. + */ + public Rectangle clearTileSelectRegion (int x, int y) + { + Rectangle drect = null; + if (_drag != null && (x != _drag.x || y != _drag.y)) { + int w = 1 + ((x > _drag.x) ? (x - _drag.x) : (_drag.x - x)); + int h = 1 + ((y > _drag.y) ? (y - _drag.y) : (_drag.y - y)); + drect = new Rectangle( + Math.min(x, _drag.x), Math.min(y, _drag.y), w, h); + } + _drag = null; + return drect; + } + + /** + * Enables or disables highlighting of the tile over which the mouse + * is currently positioned. + */ + public void enableCoordHighlighting (boolean enabled) + { + _coordHighlighting = enabled; + } + + /** + * Deletes the portal at the specified full coordinates. + */ + public void deletePortal (int x, int y) + { + Portal port = getPortal(x, y); + if (port != null) { + _scene.removePortal(port); + recomputePortals(); + recomputeVisible(); + } + } + + /** + * Returns the portal that serves as the default entrance to this + * scene or null if no default is set. + */ + public Portal getEntrance () + { + return _scene.getDefaultEntrance(); + } + + /** + * Makes the specified portal the default entrance to this scene. + */ + public void setEntrance (Portal port) + { + _scene.setDefaultEntrance(port); + } + + // documentation inherited + protected void recomputeVisible () + { + super.recomputeVisible(); + + // see if any of our visible objects overlap and mark them as bad + // monkeys; we love N^2 algorithms + for (Iterator iter = _vizobjs.iterator(); iter.hasNext(); ) { + SceneObject scobj = (SceneObject)iter.next(); + scobj.setWarning(overlaps(scobj)); + } + } + + // documentation inherited + protected void warnVisible (SceneBlock block, Rectangle sbounds) + { + // nothing doing + } + + /** Helper function for {@link #recomputeVisible}. */ + protected boolean overlaps (SceneObject tobj) + { + for (Iterator iter = _vizobjs.iterator(); iter.hasNext(); ) { + SceneObject scobj = (SceneObject)iter.next(); + if (scobj != tobj && tobj.objectFootprintOverlaps(scobj) && + tobj.getPriority() == scobj.getPriority()) { + return true; + } + } + return false; + } + + // documentation inherited + protected void paintHighlights (Graphics2D gfx, Rectangle dirty) + { + Polygon hpoly = null; + + if (_hobject != null && _hobject instanceof SceneObject) { + SceneObject scobj = (SceneObject)_hobject; + hpoly = scobj.getObjectFootprint(); + + } + + if (_emodel.getActionMode() == EditorModel.ACTION_PLACE_TILE && + (hpoly == null || + _emodel.getLayerIndex() == EditorModel.BASE_LAYER)) { + hpoly = MisoUtil.getTilePolygon(_metrics, _hcoords.x, _hcoords.y); + } + + if (hpoly != null) { + gfx.setColor(Color.green); + gfx.draw(hpoly); + } + + // paint the highlighted full coordinate + if (_coordHighlighting && _hfull.x != Integer.MIN_VALUE) { + Point spos = new Point(); + MisoUtil.fullToScreen(_metrics, _hfull.x, _hfull.y, spos); + + // set the desired stroke and color + Stroke ostroke = gfx.getStroke(); + gfx.setStroke(HIGHLIGHT_STROKE); + + // draw a red circle at the coordinate + gfx.setColor(Color.red); + gfx.draw(new Ellipse2D.Float(spos.x - 1, spos.y - 1, 3, 3)); + + // restore the original stroke + gfx.setStroke(ostroke); + } + } + + // documentation inherited + protected void paintExtras (Graphics2D gfx, Rectangle dirty) + { + super.paintExtras(gfx, dirty); + + // we don't want to paint the 'extras' stuff to the copy.. + if (gfx instanceof TGraphics2D) { + gfx = ((TGraphics2D) gfx).getPrimary(); + } + + paintPortals(gfx); + paintHighlights(gfx, dirty); + paintPlacingTile(gfx); + + // and call into any extras painters + for (int ii = 0; ii < _extras.size(); ii++) { + ((ExtrasPainter)_extras.get(ii)).paintExtras(gfx); + } + } + + /** + * Add an extras painter. + */ + protected void addExtrasPainter (ExtrasPainter painter) + { + _extras.add(painter); + } + + /** + * Remove the specified extras painter. + */ + protected void removeExtrasPainter (ExtrasPainter painter) + { + _extras.remove(painter); + } + + /** + * Paints a transparent image of the tile being placed and draws a + * highlight around the bounds of the tile's current prospective + * position. The highlight is drawn in green if the tile placement is + * valid, or red if not. + * + * @param gfx the graphics context. + */ + protected void paintPlacingTile (Graphics2D gfx) + { + // bail if we've no placing tile + if (_ptile == null || _ppos.x == Integer.MIN_VALUE) { + return; + } + + // draw a transparent rendition of the placing tile image + Composite ocomp = gfx.getComposite(); + gfx.setComposite(ALPHA_PLACING); + Shape bpoly; + if (_pscobj != null) { + _pscobj.paint(gfx); + bpoly = _pscobj.getObjectFootprint(); + + } else { + bpoly = MisoUtil.getTilePolygon(_metrics, _ppos.x, _ppos.y); + Rectangle bounds = bpoly.getBounds(); + _ptile.paint(gfx, bounds.x, bounds.y); + } + gfx.setComposite(ocomp); + + // if we're dragging, grab that footprint + if (_drag != null) { + bpoly = MisoUtil.getMultiTilePolygon(_metrics, _ppos, _drag); + } + + // draw an outline around the tile footprint + gfx.setColor(_validPlacement ? Color.blue : Color.red); + gfx.draw(bpoly); + } + + /** + * Paint demarcations at all portals in the scene. + * + * @param gfx the graphics context. + */ + protected void paintPortals (Graphics2D gfx) + { + Iterator iter = _scene.getPortals(); + while (iter.hasNext()) { + paintPortal(gfx, (EditablePortal)iter.next()); + } + } + + /** + * Paint the specified portal. + */ + protected void paintPortal (Graphics2D gfx, EditablePortal port) + { + // get the portal's center coordinate + Point spos = new Point(); + MisoUtil.fullToScreen(_metrics, port.x, port.y, spos); + int cx = spos.x, cy = spos.y; + + // translate the origin to center on the portal + gfx.translate(cx, cy); + + // rotate to reflect the portal orientation + double rot = (Math.PI / 4.0f) * port.orient; + gfx.rotate(rot); + + // draw the triangle + gfx.setColor(Color.blue); + gfx.fill(_locTri); + + // outline the triangle in black + gfx.setColor(Color.black); + gfx.draw(_locTri); + + // draw the rectangle + gfx.setColor(Color.red); + gfx.fillRect(-1, 2, 3, 3); + + // restore the original transform + gfx.rotate(-rot); + gfx.translate(-cx, -cy); + + // highlight the portal if it's the default entrance + if (port.equals(_scene.getDefaultEntrance())) { + gfx.setColor(Color.cyan); + gfx.drawRect(spos.x - 5, spos.y - 5, 10, 10); + } + } + + /** + * Updates the coordinate position and returns true if it has changed. + */ + public boolean updateCoordPos (int x, int y, Point cpos) + { + Point npos = MisoUtil.screenToFull(_metrics, x, y, new Point()); + if (!cpos.equals(npos)) { + cpos.setLocation(npos.x, npos.y); + return true; + } else { + return false; + } + } + + /** + * Returns whether placing a tile at the given coordinates in the + * scene is valid. Makes sure placing an object fits within the scene + * and doesn't overlap any other objects. + */ + protected boolean isTilePlacementValid (int x, int y, Tile tile) + { + if (tile instanceof ObjectTile) { + // create a temporary scene object for this tile + SceneObject nobj = new SceneObject(this, new ObjectInfo(0, x, y), + (ObjectTile)tile); + // report invalidity if overlaps any existing objects + int ocount = _vizobjs.size(); + for (int ii = 0; ii < ocount; ii++) { + SceneObject scobj = (SceneObject)_vizobjs.get(ii); + if (scobj.objectFootprintOverlaps(nobj)) { + return false; + } + } + } + + return true; + } + + // documentation inherited + protected boolean skipActionlessObjects () + { + return false; + } + + /** + * Converts the supplied screen coordinates into tile coordinates for + * an object tile. (See {@link #updateTileCoords}.) + * + * @return true if the tile coordinates have changed. + */ + protected boolean updateObjectTileCoords (int sx, int sy, Point tpos, + ObjectTile otile) + { + Point npos = new Point(); + MisoUtil.screenToTile(_metrics, sx, sy, npos); + adjustObjectCoordsAccordingToGrip(npos, otile); + if (!tpos.equals(npos)) { + tpos.setLocation(npos.x, npos.y); + return true; + } else { + return false; + } + } + + /** + * Alter the position of the object according to which corner + * we are holding it by. + */ + protected void adjustObjectCoordsAccordingToGrip (Point p, ObjectTile tile) + { + int dy = Math.max(3, (tile.getHeight() / _metrics.tilehei)); + int dx = Math.max(3, (tile.getWidth() / _metrics.tilewid)); + + switch (_emodel.getObjectGripDirection()) { + case DirectionCodes.NORTH: + p.x += dy; + p.y += dy; + break; + + case DirectionCodes.WEST: + p.x += dx; + break; + + case DirectionCodes.EAST: + p.y += dx; + break; + + case DirectionCodes.NORTHWEST: + p.x += dy + (dx / 2); + p.y += dy - (dx / 2); + break; + + case DirectionCodes.NORTHEAST: + p.x += dy - (dx / 2); + p.y += dy + (dx / 2); + break; + + case DirectionCodes.SOUTHWEST: + p.x += (dx / 2); + p.y -= (dx / 2); + break; + + case DirectionCodes.SOUTHEAST: + p.x -= (dx / 2); + p.y += (dx / 2); + break; + } + } + + /** + * Sets the editor model. + */ + protected void setEditorModel (EditorModel model) + { + _emodel = model; + } + + /** + * Set the scroll box that tracks our view. + */ + public void setEditorScrollBox (EditorScrollBox box) + { + _box = box; + } + + // documentation inherited + protected void paint (Graphics2D gfx, Rectangle[] dirty) + { + // if we need to refresh the box and we have all the scene data, do it + if (_refreshBox && _visiBlocks.isEmpty()) { + _refreshBox = false; + Graphics2D mini = _box.getMiniGraphics(); + Graphics2D t = new TGraphics2D(gfx, mini); + super.paint(t, dirty); + mini.dispose(); + _box.repaint(); + + } else { + // otherwise, just do a normal fast paint + super.paint(gfx, dirty); + } + } + + /** Provides access to stuff. */ + protected EditorContext _ctx; + + /** Our editor model. */ + protected EditorModel _emodel; + + /** The scrollbox that tracks our view. */ + protected EditorScrollBox _box; + + /** Do we need to refresh the image being displayed in our scrollbox? */ + protected boolean _refreshBox; + + /** We need this to create our dialogs when they are needed. */ + protected JFrame _frame; + + /** Allows scrolling horizontally. */ + protected BoundedRangeModel _horizRange = new DefaultBoundedRangeModel(); + + /** Allows scrolling vertically. */ + protected BoundedRangeModel _vertRange = new DefaultBoundedRangeModel(); + + /** The virtual screen rectangle around which we scroll. */ + protected Rectangle _area; + + /** Whether or not coordinate highlighting is enabled. */ + protected boolean _coordHighlighting; + + /** The currently highlighted full coordinate. */ + protected Point _hfull = new Point(Integer.MIN_VALUE, 0); + + /** The location of the start of a tile drag in tile coords. */ + protected Point _drag = null; + + /** The position of the tile currently being placed. */ + protected Point _ppos = new Point(Integer.MIN_VALUE, 0); + + /** Used to track whether or not the current "placing" tile is in a + * valid position. */ + protected boolean _validPlacement = false; + + /** The tile currently being placed. */ + protected Tile _ptile; + + /** Metrics for the tile currently being placed if it is an object + * tile. */ + protected SceneObject _pscobj; + + /** A list of things that will do some extra painting for us. */ + protected ArrayList _extras = new ArrayList(); + + /** The dialog providing portal edit functionality. */ + protected PortalDialog _dialogPortal; + + /** The dialog providing object edit functionality. */ + protected ObjectEditorDialog _objEditor; + + /** The object currently being edited by the object editor dialog. */ + protected SceneObject _eobject; + + /** The triangle used to render a portal on-screen. */ + protected static Polygon _locTri; + + static { + _locTri = new Polygon(); + _locTri.addPoint(-3, -3); + _locTri.addPoint(3, -3); + _locTri.addPoint(0, 3); + }; + + /** The stroke object used to draw highlighted tiles and coordinates. */ + protected static final Stroke HIGHLIGHT_STROKE = new BasicStroke(2); + + /** Alpha level used to render transparent placing tile image. */ + protected static final Composite ALPHA_PLACING = + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f); +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorScrollBox.java b/src/java/com/threerings/stage/tools/editor/EditorScrollBox.java new file mode 100644 index 000000000..36ddeabd5 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorScrollBox.java @@ -0,0 +1,174 @@ +// +// $Id: EditorScrollBox.java 19363 2005-02-17 21:36:41Z ray $ + +package com.threerings.stage.tools.editor; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Graphics; +import java.awt.Graphics2D; + +import java.awt.event.MouseEvent; + +import java.awt.geom.AffineTransform; + +import java.awt.image.BufferedImage; + +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; + +import com.samskivert.swing.ScrollBox; +import com.samskivert.swing.util.SwingUtil; + +/** + * A scrollbox that shows our position in the editor. + */ +public class EditorScrollBox extends ScrollBox +{ + /** + * Create the tightly-coupled EditorScrollBox. + */ + public EditorScrollBox (EditorScenePanel panel) + { + super(panel.getHorizModel(), panel.getVertModel()); + + _panel = panel; + _panel.setEditorScrollBox(this); + _panel.addComponentListener(new ComponentAdapter() { + public void componentResized (ComponentEvent e) + { + SwingUtil.refresh(EditorScrollBox.this); + } + }); + + createMiniMap(1, 1); + } + + /** + * Update the view of the editor map. + */ + public void updateView () + { + _updatingView = true; + _horz.setValue(_horz.getMinimum()); + _vert.setValue(_vert.getMinimum()); + } + + /** + * Get the graphics to draw into when rendering onto the minimap. + */ + public Graphics2D getMiniGraphics () + { + Graphics2D gfx = (Graphics2D) _miniMap.getGraphics(); + gfx.transform(AffineTransform.getTranslateInstance(_box.x, _box.y)); + gfx.transform(AffineTransform.getScaleInstance( + _hFactor, _vFactor)); + + if (_updatingView) { + EventQueue.invokeLater(new Runnable() { + public void run () + { + doNextPieceOfUpdate(); + } + }); + } + + return gfx; + } + + /** + * Do the next piece of the update of the view. + */ + protected void doNextPieceOfUpdate () + { + int x = _horz.getValue(); + int y = _vert.getValue(); + int lastX = _horz.getMaximum() - _horz.getExtent(); + if (x != lastX) { + x = Math.min(x + _horz.getExtent(), lastX); + } else { + x = _horz.getMinimum(); + + int lastY = _vert.getMaximum() - _vert.getExtent(); + if (y != lastY) { + y = Math.min(y + _vert.getExtent(), lastY); + } else { + // we're done + _updatingView = false; + return; + } + } + + _horz.setValue(x); + _vert.setValue(y); + } + + // documentation inherited + public Dimension getPreferredSize () + { + int horz = _horz.getMaximum() - _horz.getMinimum(); + int vert = _vert.getMaximum() - _vert.getMinimum(); + int height = MAX_HEIGHT; + int width = (int) Math.round(horz * (((float) height) / vert)); + + int maxwidth = getParent().getWidth(); + if (maxwidth > 0 && width > maxwidth) { + height = (int) Math.round(height * (((float) maxwidth) / width)); + width = maxwidth; + } + + return new Dimension(width, height); + } + + // documentation inherited + public void setBounds (int x, int y, int w, int h) + { + super.setBounds(x, y, w, h); + + if ((w != _miniMap.getWidth()) || (h != _miniMap.getHeight())) { + createMiniMap(w, h); + } + } + + // documentation inherited + protected void paintBackground (Graphics g) + { + g.drawImage(_miniMap, 0, 0, null); + } + + /** + * Create a mini map image of the correct dimensions. + */ + protected void createMiniMap (int w, int h) + { + BufferedImage newMini = new BufferedImage( + w, h, BufferedImage.TYPE_INT_ARGB); + + // TODO: copy stuff over? + _miniMap = newMini; + Graphics g = _miniMap.getGraphics(); + g.setColor(Color.black); + g.fillRect(0, 0, w, h); + g.dispose(); + } + + // documentation inherited + protected boolean isActiveButton (MouseEvent e) + { + // all buttons are ok. + return true; + } + + /** The minimap image. */ + protected BufferedImage _miniMap; + + /** The panel we box for. */ + protected EditorScenePanel _panel; + + /** Whether or not we're updating our little view. */ + protected boolean _updatingView; + + /** Our maximum height. */ + protected static final int MAX_HEIGHT = 200; +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorToolBarPanel.java b/src/java/com/threerings/stage/tools/editor/EditorToolBarPanel.java new file mode 100644 index 000000000..6eae8ef47 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorToolBarPanel.java @@ -0,0 +1,110 @@ +// +// $Id: EditorToolBarPanel.java 17780 2004-11-10 23:00:07Z ray $ + +package com.threerings.stage.tools.editor; + +import java.awt.*; +import java.awt.event.*; +import java.util.ArrayList; +import javax.swing.*; +import com.samskivert.swing.*; + +import com.threerings.media.tile.Tile; +import com.threerings.media.tile.TileIcon; +import com.threerings.media.tile.TileManager; +import com.threerings.media.tile.UniformTileSet; + +public class EditorToolBarPanel extends JPanel implements ActionListener +{ + public EditorToolBarPanel (TileManager tilemgr, EditorModel model) + { + _model = model; + + // use of flowlayout positions the toolbar and floats properly + setLayout(new FlowLayout(FlowLayout.LEFT)); + + // get our toolbar icons + UniformTileSet tbset = tilemgr.loadTileSet(ICONS_PATH, 40, 40); + + // create the toolbar + JToolBar toolbar = new JToolBar(); + + // add all of the toolbar buttons + _buttons = new ArrayList(); + for (int ii = 0; ii < EditorModel.NUM_ACTIONS; ii++) { + // get the button icon images + Tile tile = tbset.getTile(ii); + if (tile != null) { + String cmd = EditorModel.CMD_ACTIONS[ii]; + String tip = EditorModel.TIP_ACTIONS[ii]; + + // create the button + JButton b = addButton(toolbar, cmd, tip, new TileIcon(tile)); + + // add it to the set of buttons we're managing + _buttons.add(b); + + } else { + Log.warning("Unable to load toolbar icon " + + "[index=" + ii + "]."); + } + } + + // default to the first button + setSelectedButton((JButton)_buttons.get(0)); + + // add the toolbar + add(toolbar); + } + + protected JButton addButton (JToolBar toolbar, String cmd, String tip, + TileIcon icon) + { + // create the button and configure accordingly + JButton button = new JButton(new DimmedIcon(icon)); + button.setSelectedIcon(icon); + button.addActionListener(this); + button.setActionCommand("tbar_" + cmd); + button.setToolTipText(tip); + + // add the button to the toolbar + toolbar.add(button); + + return button; + } + + protected void setSelectedButton (JButton button) + { + for (int ii = 0; ii < _buttons.size(); ii++) { + JButton tb = (JButton)_buttons.get(ii); + tb.setSelected(tb == button); + } + } + + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); + + if (cmd.startsWith("tbar")) { + + // select the chosen mode in the toolbar + setSelectedButton((JButton)e.getSource()); + + // update the active mode in the model, stripping the + // "tbar_" prefix from the command string + _model.setActionMode(cmd.substring(5)); + + } else { + Log.warning("Unknown action command [cmd=" + cmd + "]."); + } + } + + /** The buttons in the tool bar. */ + protected ArrayList _buttons; + + /** The editor data model. */ + protected EditorModel _model; + + protected static final String ICONS_PATH = + "media/stage/tools/editor/toolbar_icons.png"; +} diff --git a/src/java/com/threerings/stage/tools/editor/Log.java b/src/java/com/threerings/stage/tools/editor/Log.java new file mode 100644 index 000000000..dcfd0ae79 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/Log.java @@ -0,0 +1,38 @@ +// +// $Id: Log.java 8859 2003-05-16 20:05:45Z mdb $ + +package com.threerings.stage.tools.editor; + +/** + * A placeholder class that contains a reference to the log object used by + * this package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("stage.tools.editor"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/stage/tools/editor/ObjectEditorDialog.java b/src/java/com/threerings/stage/tools/editor/ObjectEditorDialog.java new file mode 100644 index 000000000..49a8d0288 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/ObjectEditorDialog.java @@ -0,0 +1,245 @@ + +// $Id: ObjectEditorDialog.java 16959 2004-08-30 22:09:53Z ray $ + +package com.threerings.stage.tools.editor; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Arrays; + +import javax.swing.BorderFactory; +import javax.swing.JComboBox; +import javax.swing.JInternalFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSlider; +import javax.swing.JTextField; + +import com.samskivert.swing.HGroupLayout; +import com.samskivert.swing.VGroupLayout; +import com.samskivert.util.StringUtil; + +import com.threerings.media.image.ColorPository.ColorRecord; +import com.threerings.media.image.ColorPository; + +import com.threerings.media.tile.NoSuchTileSetException; +import com.threerings.media.tile.RecolorableTileSet; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileUtil; + +import com.threerings.miso.client.SceneObject; + +import com.threerings.stage.tools.editor.util.EditorContext; +import com.threerings.stage.tools.editor.util.EditorDialogUtil; + +/** + * Used to edit object attributes. + */ +public class ObjectEditorDialog extends JInternalFrame + implements ActionListener +{ + public ObjectEditorDialog (EditorContext ctx, EditorScenePanel panel) + { + super("Edit object attributes", true); + _ctx = ctx; + _panel = panel; + + // get a handle on the top-level panel + JPanel top = (JPanel)getContentPane(); + + // set up a layout manager for the panel + VGroupLayout gl = new VGroupLayout( + VGroupLayout.STRETCH, VGroupLayout.STRETCH, 5, VGroupLayout.CENTER); + top.setLayout(gl); + top.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + // object action editor elements + JPanel sub = new JPanel(new HGroupLayout(HGroupLayout.STRETCH)); + sub.add(new JLabel("Object action command:"), HGroupLayout.FIXED); + sub.add(_action = new JTextField()); + _action.addActionListener(this); + _action.setActionCommand("ok"); + top.add(sub); + + // create the priority slider + sub = new JPanel(new HGroupLayout(HGroupLayout.STRETCH)); + sub.add(new JLabel("Render priority:"), HGroupLayout.FIXED); + sub.add(_priority = new JSlider(-5, 5)); + _priority.setMajorTickSpacing(5); + _priority.setMinorTickSpacing(1); + _priority.setPaintTicks(true); + top.add(sub); + + // create colorization selectors + JPanel zations = HGroupLayout.makeButtonBox(HGroupLayout.LEFT); + zations.add(new JLabel("Colorizations:")); + zations.add(_primary = new JComboBox(NO_CHOICES)); + zations.add(_secondary = new JComboBox(NO_CHOICES)); + top.add(zations); + + // create our OK/Cancel buttons + sub = HGroupLayout.makeButtonBox(HGroupLayout.CENTER); + EditorDialogUtil.addButton(this, sub, "OK", "ok"); + EditorDialogUtil.addButton(this, sub, "Cancel", "cancel"); + top.add(sub); + + pack(); + } + + /** + * Prepare the dialog for display. This method should be called + * before display() is called. + * + * @param scobj the object to edit. + */ + public void prepare (SceneObject scobj) + { + _scobj = scobj; + + // set our title to the name of the tileset and the tile index + String title; + int tsid = TileUtil.getTileSetId(scobj.info.tileId); + int tidx = TileUtil.getTileIndex(scobj.info.tileId); + TileSet tset = null; + try { + tset = _ctx.getTileManager().getTileSet(tsid); + title = tset.getName() + ": " + tidx; + } catch (NoSuchTileSetException nstse) { + title = "Error(" + tsid + "): " + tidx; + } + title += " (" + StringUtil.coordsToString( + _scobj.info.x, _scobj.info.y) + ")"; + setTitle(title); + + // configure our elements + String atext = (scobj.info.action == null ? "" : scobj.info.action); + _action.setText(atext); + _priority.setValue(scobj.getPriority()); + + // if the object supports colorizations, configure those + boolean haveZations = false; + Object[] pzations = null; + Object[] szations = null; + if (tset != null) { + String[] zations = null; + if (tset instanceof RecolorableTileSet) { + zations = ((RecolorableTileSet)tset).getColorizations(); + } + if (zations != null) { + pzations = computeZations(zations, 0); + szations = computeZations(zations, 1); + } + } + configureZations(_primary, pzations, _scobj.info.getPrimaryZation()); + configureZations(_secondary, szations, + _scobj.info.getSecondaryZation()); + + // select the text edit field and focus it + _action.setCaretPosition(0); + _action.moveCaretPosition(atext.length()); + _action.requestFocus(); + } + + protected Object[] computeZations (String[] zations, int index) + { + if (zations.length <= index || StringUtil.blank(zations[index])) { + return null; + } + ColorPository cpos = _ctx.getColorPository(); + ColorRecord[] crecs = cpos.enumerateColors(zations[index]); + if (crecs == null) { + return null; + } + Object[] czations = new Object[crecs.length+1]; + czations[0] = new ZationChoice(0, "none"); + for (int ii = 0; ii < crecs.length; ii++) { + czations[ii+1] = + new ZationChoice(crecs[ii].colorId, crecs[ii].name); + } + Arrays.sort(czations); + return czations; + } + + protected void configureZations ( + JComboBox combo, Object[] zations, int colorId) + { + int selidx = 0; + combo.setEnabled(zations != null); + if (zations != null) { + combo.removeAllItems(); + for (int ii = 0; ii < zations.length; ii++) { + combo.addItem(zations[ii]); + if (((ZationChoice)zations[ii]).colorId == colorId) { + selidx = ii; + } + } + } + combo.setSelectedIndex(selidx); + } + + // documentation inherited from interface + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); + + if (cmd.equals("ok")) { + _scobj.info.action = _action.getText(); + byte prio = (byte)_priority.getValue(); + if (prio != _scobj.getPriority()) { + _scobj.setPriority(prio); + } + + int ozations = _scobj.info.zations; + ZationChoice pchoice = (ZationChoice)_primary.getSelectedItem(); + ZationChoice schoice = (ZationChoice)_secondary.getSelectedItem(); + _scobj.info.setZations(pchoice.colorId, schoice.colorId); + if (ozations != _scobj.info.zations) { + _scobj.refreshObjectTile(_panel); + } + + _panel.objectEditorDismissed(); + + } else if (cmd.equals("cancel")) { + // do nothing except hide the dialog + _panel.objectEditorDismissed(); + + } else { + System.err.println("Received unknown action: " + e); + return; + } + + // hide the dialog + EditorDialogUtil.dismiss(this); + } + + /** Used to display colorization choices. */ + protected static class ZationChoice + implements Comparable + { + public short colorId; + public String name; + + public ZationChoice (int colorId, String name) { + this.colorId = (short)colorId; + this.name = name; + } + + public int compareTo (Object other) { + return colorId - ((ZationChoice)other).colorId; + } + + public String toString () { + return name; + } + } + + protected EditorContext _ctx; + protected EditorScenePanel _panel; + protected JTextField _action; + protected JSlider _priority; + protected SceneObject _scobj; + protected JComboBox _primary, _secondary; + + protected static final ZationChoice[] NO_CHOICES = new ZationChoice[] { + new ZationChoice(0, "none") }; +} diff --git a/src/java/com/threerings/stage/tools/editor/PortalDialog.java b/src/java/com/threerings/stage/tools/editor/PortalDialog.java new file mode 100644 index 000000000..732c0b34e --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/PortalDialog.java @@ -0,0 +1,164 @@ +// +// $Id: PortalDialog.java 9371 2003-06-02 20:28:21Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; + +import com.samskivert.swing.*; + +import com.threerings.whirled.spot.data.Portal; +import com.threerings.whirled.spot.tools.EditablePortal; + +import com.threerings.stage.data.StageScene; +import com.threerings.stage.tools.editor.util.EditorDialogUtil; + +/** + * The PortalDialog is used to present the user with a dialog + * allowing them to enter the information associated with an + * EditablePortal. The dialog is used both to set up a new + * portal and to edit an existing portal. + */ +public class PortalDialog extends JInternalFrame + implements ActionListener +{ + /** + * Constructs the portal dialog. + */ + public PortalDialog () + { + super("Edit Portal", true); + + // get a handle on the top-level panel + JPanel top = (JPanel)getContentPane(); + + // set up a layout manager for the panel + GroupLayout gl = new VGroupLayout(GroupLayout.STRETCH); + gl.setOffAxisPolicy(GroupLayout.STRETCH); + top.setLayout(gl); + top.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + // add the dialog instruction text + top.add(new JLabel("Enter settings for this portal:")); + + // create a panel to contain the portal name info + JPanel sub = new JPanel(new HGroupLayout(GroupLayout.STRETCH)); + sub.add(new JLabel("Portal name:", SwingConstants.RIGHT)); + + // create and add the portal name text entry field + sub.add(_portalText = new JTextField()); + + // add the portal name info to the top-level panel + top.add(sub); + + // create a check box to allow making this the default + // entrance portal + _entrance = new JCheckBox("Default Entrance"); + _entrance.addActionListener(this); + _entrance.setActionCommand("entrance"); + top.add(_entrance); + + // create a panel to contain the OK/Cancel buttons + sub = new JPanel(new HGroupLayout()); + EditorDialogUtil.addButton(this, sub, "OK", "ok"); + + // add the buttons to the top-level panel + top.add(sub); + + pack(); + } + + /** + * Prepare the dialog for display. This method should be called + * before display() is called. + * + * @param port the portal to edit. + */ + public void prepare (StageScene scene, EditablePortal port) + { + _port = port; + _scene = scene; + + // if the location is already a portal, fill the text entry field + // with the current scene name, else clear it + String text = port.name; + _portalText.setText(text); + + // select the text edit field + _portalText.setCaretPosition(0); + _portalText.moveCaretPosition(text.length()); + + // select the default entrance check box appropriately + Portal entry = _scene.getDefaultEntrance(); + _entrance.setSelected(entry == null || + entry.portalId == _port.portalId); + + // request the keyboard focus so that the destination scene + // name can be typed immediately + _portalText.requestFocus(); + } + + /** + * Handle action events on the dialog user interface elements. + */ + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); + + if (cmd.equals("entrance")) { + _entrance.setSelected(_entrance.isSelected()); + + } else if (cmd.equals("ok")) { + handleSubmit(); + + } else { + Log.warning("Unknown action command [cmd=" + cmd + "]."); + } + } + + /** + * Handles the user submitting the dialog via the "OK" button. + */ + protected void handleSubmit () + { + // get the destination scene name + _port.name = _portalText.getText(); + + // update the scene's default entrance + if (_entrance.isSelected()) { + _scene.setDefaultEntrance(_port); + + } else if (_scene.getDefaultEntrance() == _port) { + _scene.setDefaultEntrance(null); + } + + // hide the dialog + EditorDialogUtil.dismiss(this); + } + + // documentation inherited + protected void processKeyEvent (KeyEvent e) + { + switch (e.getKeyCode()) { + case KeyEvent.VK_ENTER: handleSubmit(); break; + case KeyEvent.VK_ESCAPE: setVisible(false); break; + } + } + + /** The scene. */ + protected StageScene _scene; + + /** The portal name text entry field. */ + protected JTextField _portalText; + + /** The portal default entrance check box. */ + protected JCheckBox _entrance; + + /** The location object denoting the portal location. */ + protected EditablePortal _port; + + /** The combo box listing the direction orientations. */ + protected JComboBox _orientcombo; +} diff --git a/src/java/com/threerings/stage/tools/editor/PortalTool.java b/src/java/com/threerings/stage/tools/editor/PortalTool.java new file mode 100644 index 000000000..0757b6e3b --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/PortalTool.java @@ -0,0 +1,161 @@ +// +// $Id: PortalTool.java 20143 2005-03-30 01:12:48Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.event.MouseEvent; +import javax.swing.event.MouseInputAdapter; + +import java.util.Iterator; + +import com.threerings.miso.util.MisoUtil; +import com.threerings.util.DirectionCodes; +import com.threerings.util.DirectionUtil; + +import com.threerings.whirled.spot.tools.EditablePortal; + +import com.threerings.stage.data.StageScene; +import com.threerings.stage.tools.editor.util.ExtrasPainter; + +/** + * A tool for setting a portal. + */ +public class PortalTool extends MouseInputAdapter + implements ExtrasPainter +{ + public void init (EditorScenePanel panel, int centerx, int centery) + { + _panel = panel; + _scene = (StageScene)_panel.getScene(); + + // adjust the center screen coordinate so that it is positioned at + // the closest full coordinate where a portal may be placed + Point p = _panel.getFullCoords(centerx, centery); + + // configure the portal with these full coordinates + _portal = new EditablePortal(); + _portal.x = p.x; + _portal.y = p.y; + + // we want to listen to drags and clicks + _panel.addMouseListener(this); + _panel.addMouseMotionListener(this); + _panel.addExtrasPainter(this); + + // now map that back to a screen coordinate + _center = _panel.getScreenCoords(p.x, p.y); + + calculateOrientation(centerx, centery); + } + + /** + * When the mouse is dragged we update the current portal. + */ + public void mouseDragged (MouseEvent event) + { + calculateOrientation(event.getX(), event.getY()); + } + + /** + * If button1 is released, we store the new portal, if button3: we + * cancel. + */ + public void mouseReleased (MouseEvent event) + { + switch (event.getButton()) { + case MouseEvent.BUTTON1: + savePortal(); + // fall through to BUTTON3 + + case MouseEvent.BUTTON3: + dispose(); + break; + } + } + + /** + * Paint what the clusters would look like if we were to add them to + * the scene. + */ + public void paintExtras (Graphics2D gfx) + { + _panel.paintPortal(gfx, _portal); + } + + /** + * Updates the orientation of the portal based on the current mouse + * position and potentially repaints. + */ + protected void calculateOrientation (int x, int y) + { + // get the orientation towards the center from the current x/y + int norient = MisoUtil.getProjectedIsoDirection( + x, y, _center.x, _center.y); + norient = DirectionUtil.getOpposite(norient); + if (norient == DirectionCodes.NONE) { + norient = DirectionCodes.NORTH; + } + + // if our orientation changed, repaint + if (norient != _portal.orient) { + _portal.orient = (byte)norient; + _panel.repaint(); + } + } + + /** + * Adds our portal to the scene. + */ + protected void savePortal () + { + // try to come up with a reasonable name + String dirname = DirectionUtil.toString(_portal.orient).toLowerCase(); + String name = dirname; + for (int ii=1; portalNameExists(name); ii++) { + name = dirname + ii; + } + _portal.name = name; + + // add the portal to the scene and pop up the editor dialog + _scene.addPortal(_portal); + _panel.editPortal(_portal); + } + + /** + * See if the portal name already exists. + */ + protected boolean portalNameExists (String name) + { + Iterator iter = _scene.getPortals(); + while (iter.hasNext()) { + if (((EditablePortal)iter.next()).name.equals(name)) { + return true; + } + } + return false; + } + + /** + * Get rid of ourselves. + */ + protected void dispose () + { + _panel.removeMouseListener(this); + _panel.removeMouseMotionListener(this); + _panel.removeExtrasPainter(this); + } + + /** The panel. */ + protected EditorScenePanel _panel; + + /** The scene. */ + protected StageScene _scene; + + /** Center coordinates. */ + protected Point _center; + + /** Our newly created portal. */ + protected EditablePortal _portal; +} diff --git a/src/java/com/threerings/stage/tools/editor/PreferencesDialog.java b/src/java/com/threerings/stage/tools/editor/PreferencesDialog.java new file mode 100644 index 000000000..5b6a11a09 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/PreferencesDialog.java @@ -0,0 +1,103 @@ +// +// $Id: PreferencesDialog.java 9938 2003-06-20 03:55:58Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JInternalFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.samskivert.swing.GroupLayout; +import com.samskivert.swing.HGroupLayout; +import com.samskivert.swing.VGroupLayout; + +import com.threerings.stage.tools.editor.util.EditorDialogUtil; + +/** + * A dialog for editing preferences. + */ +public class PreferencesDialog extends JInternalFrame + implements ActionListener +{ + /** + * Creates a preferences dialog. + */ + public PreferencesDialog () + { + super("Editor Preferences", true); + + // set up a layout manager for the panel + JPanel top = (JPanel)getContentPane(); + GroupLayout gl = new VGroupLayout(GroupLayout.STRETCH); + gl.setOffAxisPolicy(GroupLayout.STRETCH); + top.setLayout(gl); + top.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + // create the display for the test tile directory pref + JPanel sub = new JPanel(new HGroupLayout(GroupLayout.STRETCH)); + sub.add(new JLabel("Directory to search for test tiles:"), + GroupLayout.FIXED); + EditorDialogUtil.addButton(this, sub, + EditorConfig.getTestTileDirectory(), "testtiledir"); + top.add(sub); + + sub = new JPanel(new HGroupLayout()); + EditorDialogUtil.addButton(this, sub, "OK", "ok"); + top.add(sub); + + pack(); + } + + /** + * Handle action events. + */ + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); + + if (cmd.equals("testtiledir")) { + changeTestTileDir((JButton) e.getSource()); + + } else if (cmd.equals("ok")) { + EditorDialogUtil.dispose(this); + + } else { + Log.warning("Unknown action command [cmd=" + cmd + "]."); + } + } + + /** + * Pop up a file selection box for specifying the directory to look + * in for test tiles. + */ + protected void changeTestTileDir (JButton button) + { + JFileChooser chooser; + + // figure out which + File f = new File(button.getText()); + if (!f.exists()) { + chooser = new JFileChooser(); + } else { + chooser = new JFileChooser(f); + } + + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setSelectedFile(f); + + int result = chooser.showDialog(this, "Select"); + if (JFileChooser.APPROVE_OPTION == result) { + f = chooser.getSelectedFile(); + String newdir = f.getPath(); + button.setText(newdir); + EditorConfig.setTestTileDirectory(newdir); + } + } +} diff --git a/src/java/com/threerings/stage/tools/editor/SceneInfoPanel.java b/src/java/com/threerings/stage/tools/editor/SceneInfoPanel.java new file mode 100644 index 000000000..6e65168f3 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/SceneInfoPanel.java @@ -0,0 +1,288 @@ +// +// $Id: SceneInfoPanel.java 20143 2005-03-30 01:12:48Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; + +import java.util.HashSet; +import java.util.Iterator; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; + +import com.samskivert.swing.GroupLayout; +import com.samskivert.swing.HGroupLayout; +import com.samskivert.util.CollectionUtil; +import com.samskivert.util.SortableArrayList; +import com.samskivert.util.SortedIterator; + +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.data.SparseMisoSceneModel.ObjectVisitor; + +import com.threerings.media.image.ColorPository; + +import com.threerings.media.tile.NoSuchTileSetException; +import com.threerings.media.tile.RecolorableTileSet; +import com.threerings.media.tile.TileManager; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileUtil; + +import com.threerings.stage.data.StageMisoSceneModel; +import com.threerings.stage.data.StageScene; + +import com.threerings.stage.tools.editor.util.EditorContext; + +/** + * The scene info panel presents the user with options to select the + * scene layer to edit, and whether to display tile coordinates and + * locations in the scene view. + */ +public class SceneInfoPanel extends JPanel + implements ActionListener +{ + /** + * Constructs the scene info panel. + */ + public SceneInfoPanel (EditorContext ctx, EditorModel model, + EditorScenePanel svpanel) + { + _ctx = ctx; + _svpanel = svpanel; + + // configure the panel + GroupLayout gl = new HGroupLayout(); + gl.setGap(12); + setLayout(gl); + + JPanel vert = GroupLayout.makeVStretchBox(5); + + JPanel hbox = GroupLayout.makeHBox(); + + // create a panel for the name label + hbox.add(createLabel("Scene name:", _scenename = new JTextField(10))); + _scenename.addActionListener(this); + _scenename.addFocusListener(new FocusAdapter() { + public void focusLost (FocusEvent e) { + _scenename.postActionEvent(); + } + }); + + // create a drop-down for selecting the scene type + SortableArrayList types = new SortableArrayList(); + ctx.enumerateSceneTypes(types); + types.sort(); + types.add(0, ""); + hbox.add(createLabel("Scene type:", + _scenetype = new JComboBox(types.toArray()))); + _scenetype.addActionListener(this); + + vert.add(hbox); + + // set up the scene colorization stuff + hbox = GroupLayout.makeButtonBox(GroupLayout.LEFT); + hbox.add(_colorClasses = new JComboBox()); + hbox.add(_colorIds = new JComboBox()); + vert.add(createLabel("Colorizations:", hbox), GroupLayout.FIXED); + _colorClasses.addActionListener(this); + _colorIds.addActionListener(this); + _colorClasses.addPopupMenuListener(new PopupMenuListener() { + public void popupMenuCanceled (PopupMenuEvent e) { } + public void popupMenuWillBecomeInvisible (PopupMenuEvent e) { } + public void popupMenuWillBecomeVisible (PopupMenuEvent e) { + recomputeColorClasses(); + } + }); + + add(vert, GroupLayout.FIXED); + } + + protected JPanel createLabel (String label, JComponent comp) + { + JPanel panel = GroupLayout.makeButtonBox(GroupLayout.CENTER); + panel.add(new JLabel(label), GroupLayout.FIXED); + panel.add(comp); + return panel; + } + + /** + * Called when the scene in the editor changes. + */ + public void setScene (StageScene scene) + { + _scene = scene; + _scenename.setText(scene.getName()); + _scenetype.setSelectedItem(scene.getType()); + + recomputeColorClasses(); + } + + // documentation inherited from interface ActionListener + public void actionPerformed (ActionEvent e) + { + Object src = e.getSource(); + if (src == _scenename) { + _scene.setName(_scenename.getText().trim()); + + } else if (src == _scenetype) { + _scene.setType((String) _scenetype.getSelectedItem()); + + } else if (src == _colorClasses) { + String cclass = (String) _colorClasses.getSelectedItem(); + configureColorIds(cclass); + + } else if (src == _colorIds) { + setNewDefaultColor(); + } + } + + /** + * Prior to the color classes popup popping up, recompute the possible + * values. + */ + protected void recomputeColorClasses () + { + // add all possible colorization names to the list + final TileManager tilemgr = _ctx.getTileManager(); + final HashSet set = new HashSet(); + StageMisoSceneModel msmodel = StageMisoSceneModel.getSceneModel( + _scene.getSceneModel()); + msmodel.visitObjects(new ObjectVisitor() { + public void visit (ObjectInfo info) { + int tsid = TileUtil.getTileSetId(info.tileId); + TileSet tset; + try { + tset = tilemgr.getTileSet(tsid); + } catch (NoSuchTileSetException nstse) { + return; + } + String[] zations; + if (tset instanceof RecolorableTileSet) { + zations = ((RecolorableTileSet) tset).getColorizations(); + } else { + return; + } + if (zations != null) { + for (int ii=0; ii < zations.length; ii++) { + set.add(zations[ii]); + } + } + } + }); + + Object selected = _colorClasses.getSelectedItem(); + DefaultComboBoxModel model = + (DefaultComboBoxModel) _colorClasses.getModel(); + model.removeAllElements(); + for (Iterator itr = new SortedIterator(set); itr.hasNext(); ) { + model.addElement(itr.next()); + } + if (selected != null) { + _colorClasses.setSelectedItem(selected); + } + } + + /** + * Show which color Ids are available for the currently selected + * colorization class, and select the selected one. + */ + protected void configureColorIds (String cclass) + { + _colorIds.removeActionListener(this); + try { + DefaultComboBoxModel model = + (DefaultComboBoxModel) _colorIds.getModel(); + model.removeAllElements(); + if (cclass == null) { + return; + } + + ColorPository cpos = _ctx.getColorPository(); + + String noChoice = ""; + String choice = noChoice; + + ColorPository.ClassRecord classRec = cpos.getClassRecord(cclass); + int pick = _scene.getDefaultColor(classRec.classId); + + ColorPository.ColorRecord[] colors = cpos.enumerateColors(cclass); + SortableArrayList list = new SortableArrayList(); + for (int ii=0; ii < colors.length; ii++) { + list.insertSorted(colors[ii].name); + if (colors[ii].colorId == pick) { + choice = colors[ii].name; + } + } + + model.addElement(noChoice); + for (int ii=0; ii < list.size(); ii++) { + model.addElement(list.get(ii)); + } + _colorIds.setSelectedItem(choice); + + } finally { + _colorIds.addActionListener(this); + } + } + + /** + * Called when a _colorIds color is selected. + */ + protected void setNewDefaultColor () + { + String cclass = (String) _colorClasses.getSelectedItem(); + if (cclass == null) { + return; + } + + ColorPository cpos = _ctx.getColorPository(); + ColorPository.ClassRecord classRec = cpos.getClassRecord(cclass); + ColorPository.ColorRecord[] colors = cpos.enumerateColors(cclass); + Object selected = _colorIds.getSelectedItem(); + int pick = -1; + + for (int ii=0; ii < colors.length; ii++) { + if (colors[ii].name.equals(selected)) { + pick = colors[ii].colorId; + break; + } + } + + // only update the scene if our selection has actually changed + if (_scene.getDefaultColor(classRec.classId) != pick) { + _scene.setDefaultColor(classRec.classId, pick); + _svpanel.setScene(_scene); + _svpanel.repaint(); + } + } + + /** The giver of life. */ + protected EditorContext _ctx; + + /** The scene we're controlling. */ + protected StageScene _scene; + + /** The scene name entry field. */ + protected JTextField _scenename; + + /** The scene type selector. */ + protected JComboBox _scenetype, _colorClasses, _colorIds; + + /** The scene panel, which we hackily repaint when default colors change. */ + protected EditorScenePanel _svpanel; + + /** The object grip direction button. */ + protected DirectionButton _dirbutton; +} diff --git a/src/java/com/threerings/stage/tools/editor/TestTileLoader.java b/src/java/com/threerings/stage/tools/editor/TestTileLoader.java new file mode 100644 index 000000000..166260a75 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/TestTileLoader.java @@ -0,0 +1,173 @@ +// +// $Id: TestTileLoader.java 9938 2003-06-20 03:55:58Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileFilter; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import javax.imageio.ImageIO; + +import com.samskivert.util.HashIntMap; + +import com.threerings.media.tile.ImageProvider; +import com.threerings.media.tile.SimpleCachingImageProvider; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileSetIDBroker; + +import com.threerings.miso.tile.tools.xml.BaseTileSetRuleSet; +import com.threerings.media.tile.tools.xml.ObjectTileSetRuleSet; +import com.threerings.media.tile.tools.xml.SwissArmyTileSetRuleSet; +import com.threerings.media.tile.tools.xml.XMLTileSetParser; + +/** + * The TestTileLoader handles test tiles. Test tiles are tiles that an + * artist can load in on-the-fly to see how things look in the scene editor. + */ +public class TestTileLoader implements TileSetIDBroker +{ + /** + * Construct the TestTileLoader. + */ + public TestTileLoader () + { + // our xml parser + _parser = new XMLTileSetParser(); + // add some rulesets + _parser.addRuleSet("bundle/base", new BaseTileSetRuleSet()); + _parser.addRuleSet("bundle/object", new ObjectTileSetRuleSet()); + + // we used to parse fringes, but we don't anymore + //_parser.addRuleSet("bundle/fringe", new SwissArmyTileSetRuleSet()); + } + + /** + * Check the specified directory and all its subdirectories for xml files. + * Each directory should contain at most one xml file, each xml file + * should specify at most one tileset. That tileset specification + * will be used to create tilesets for all the .png files in the same + * directory. + * + * @return a HashIntMap containing a TileSetId -> TileSet mapping for + * all the tilesets we create. + */ + public HashIntMap loadTestTiles () + { + String directory = EditorConfig.getTestTileDirectory(); + HashIntMap map = new HashIntMap(); + + // recurse test directory, making a tileset from the xml file inside + // and cloning it for each image we find in there. + File testdir = new File(directory); + // make sure it's a directory + if (!testdir.isDirectory()) { + Log.warning("Test tileset directory is not actually a directory: " + + directory); + return map; + } + + // recursively load all the test tiles + loadTestTilesFromDir(testdir, map); + + return map; + } + + /** + * Load xml tile sets from a directory. + */ + protected void loadTestTilesFromDir (File directory, + HashIntMap sets) + { + // first recurse + File[] subdirs = directory.listFiles(new FileFilter() { + public boolean accept (File f) { + return f.isDirectory(); + } + }); + for (int ii=0; ii < subdirs.length; ii++) { + loadTestTilesFromDir(subdirs[ii], sets); + } + + // now look for the xml file + String[] xml = directory.list(new FilenameFilter() { + public boolean accept (File dir, String name) { + return name.endsWith(".xml"); + } + }); + + for (int ii=0; ii < xml.length; ii++) { + File xmlfile = new File(directory, xml[ii]); + + HashMap tiles = new HashMap(); + try { + _parser.loadTileSets(xmlfile, tiles); + } catch (IOException ioe) { + Log.warning("Error while parsing " + xmlfile.getPath()); + Log.logStackTrace(ioe); + continue; + } + + Iterator iter = tiles.values().iterator(); + while (iter.hasNext()) { + TileSet ts = (TileSet) iter.next(); + String path = new File(directory, ts.getImagePath()).getPath(); + + // before we insert, make sure we can load the image + if (null != _improv.getTileSetImage(path, null)) { + ts.setImageProvider(_improv); + ts.setImagePath(path); + sets.put(getTileSetID(path), ts); + } + } + } + } + + /** + * Generate unique and completely fake tileset IDs that will be stable + * even after a reload of test tiles. + */ + public int getTileSetID (String tileSetPath) + { + Integer id = (Integer) _idmap.get(tileSetPath); + if (null == id) { + id = new Integer(_fakeID--); + _idmap.put(tileSetPath, id); + } + return id.intValue(); + } + + // documentation inherited + public boolean tileSetMapped (String tilesetPath) + { + return _idmap.containsKey(tilesetPath); + } + + /** + * Since we're just testing, we don't save these crazy IDs. + */ + public void commit () + { + // this method does nothing. perhaps it should be called "committee". + } + + /** The value of the next fakeID we'll hand out. */ + protected int _fakeID = -1; + + /** A mapping of pathname -> tileset id. */ + protected HashMap _idmap = new HashMap(); + + /** Our xml parser. */ + protected XMLTileSetParser _parser; + + /** Our image provider. */ + protected ImageProvider _improv = new SimpleCachingImageProvider() { + protected BufferedImage loadImage (String path) + throws IOException { + return ImageIO.read(new File(path)); + } + }; +} diff --git a/src/java/com/threerings/stage/tools/editor/TileInfoPanel.java b/src/java/com/threerings/stage/tools/editor/TileInfoPanel.java new file mode 100644 index 000000000..1a98f795f --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/TileInfoPanel.java @@ -0,0 +1,707 @@ +// +// $Id: TileInfoPanel.java 17625 2004-10-28 17:50:03Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Image; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +import java.util.ArrayList; +import java.util.Iterator; + +import javax.swing.BorderFactory; +import javax.swing.DefaultListCellRenderer; +import javax.swing.DefaultListModel; +import javax.swing.ImageIcon; +import javax.swing.JList; +import javax.swing.JSplitPane; +import javax.swing.JTable; +import javax.swing.JTree; +import javax.swing.ListSelectionModel; +import javax.swing.border.Border; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableColumn; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import com.samskivert.util.HashIntMap; +import com.samskivert.util.QuickSort; +import com.samskivert.util.StringUtil; + +import com.threerings.media.SafeScrollPane; + +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileSetRepository; + +import com.threerings.stage.tools.editor.util.EditorContext; +import com.threerings.stage.tools.editor.util.TileSetUtil; + +/** + * The tile info panel presents the user with options to select the + * tile to be applied to the scene. + */ +public class TileInfoPanel extends JSplitPane + implements ListSelectionListener, TreeSelectionListener +{ + /** + * Constructs the tile info panel. + */ + public TileInfoPanel (EditorContext ctx, EditorModel model) + { + TileSetRepository tsrepo = ctx.getTileSetRepository(); + + // set up our key observers + registerKeyListener(ctx); + + _model = model; + + // we're going to sort all of the available tilesets into those + // which are applicable to each layer + try { + _layerSets = new ArrayList[2]; + _layerLengths = new int[2]; + for (int ii=0; ii < 2; ii++) { + _layerSets[ii] = new ArrayList(); + } + + Iterator tsids = tsrepo.enumerateTileSetIds(); + while (tsids.hasNext()) { + Integer tsid = (Integer)tsids.next(); + TileSet set = tsrepo.getTileSet(tsid.intValue()); + + // determine which layer to which this tileset applies + int lidx = TileSetUtil.getLayerIndex(set); + if (lidx != -1) { + _layerSets[lidx].add( + new TileSetRecord(lidx, tsid.intValue(), set)); + } + } + + for (int ii=0; ii < 2; ii++) { + _layerLengths[ii] = _layerSets[ii].size(); + } + + } catch (Exception e) { + Log.warning("Error enumerating tilesets."); + Log.logStackTrace(e); + } + + // set up a border denoting our contents + Border border = BorderFactory.createEtchedBorder(); + setBorder(BorderFactory.createTitledBorder(border, "Tile Info")); + + // create a tree for selecting tileset + DefaultMutableTreeNode root = new DefaultMutableTreeNode(""); + _tsettree = new JTree(root); + + // don't draw any funny little icons in the tree + DefaultTreeCellRenderer cellrend = + (DefaultTreeCellRenderer) _tsettree.getCellRenderer(); + cellrend.setLeafIcon(null); + cellrend.setOpenIcon(null); + cellrend.setClosedIcon(null); + + // tree- only let one thing be selected, and let us know when it haps + _tsettree.getSelectionModel().setSelectionMode( + TreeSelectionModel.SINGLE_TREE_SELECTION); + _tsettree.addTreeSelectionListener(this); + + // create a scrollpane to hold the tree + SafeScrollPane scrolly = new SafeScrollPane(_tsettree); + scrolly.setVerticalScrollBarPolicy( + SafeScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + + DefaultListModel qmodel = new DefaultListModel(); + for (int ii=0; ii < 10; ii++) { + qmodel.addElement(""); + } + _quickList = new JList(qmodel); + _quickList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + _quickList.addListSelectionListener(this); + _quickList.setCellRenderer(new DefaultListCellRenderer() { + public Component getListCellRendererComponent ( + JList list, Object value, int index, boolean isSelected, + boolean cellHasFocus) + { + // put the key number in front of each element + Component result = super.getListCellRendererComponent( + list, value, index, isSelected, cellHasFocus); + setText("" + ((index + 11) % 10) + ". " + getText()); + return result; + } + }); + JSplitPane leftSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + leftSplit.setTopComponent(new SafeScrollPane(_quickList)); + leftSplit.setBottomComponent(scrolly); + + // add the west side + setLeftComponent(leftSplit); + + // create a table to display the tiles in the selected tileset + _tiletable = new JTable(_tablemodel = new TileTableModel()); + _tiletable.getSelectionModel().addListSelectionListener(this); + _tiletable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + updateTileTable(); + + // wrap the table in a scrollpane for lengthy tilesets + _scroller = new SafeScrollPane(_tiletable); + _scroller.setVerticalScrollBarPolicy( + SafeScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + + // add the tile table as the entire east side + setRightComponent(_scroller); + + // add the tilesets and select the starting tile set + updateTileSetTree(); + + // The damn splitpane freaks out unless we do this + setDividerLocation(230); + } + + /** + * Register key listeners to do things with the quick list. + */ + protected void registerKeyListener (EditorContext ctx) + { + ctx.getKeyDispatcher().addGlobalKeyListener(new KeyAdapter() { + public void keyTyped (KeyEvent e) + { + char keychar = e.getKeyChar(); + if ((keychar < '0') || (keychar > '9')) { + return; + } + + // turn 1 into 0, and 0 into 9 + int index = (keychar - '0' + 9) % 10; + if (e.isControlDown() || e.isAltDown()) { + // add + if (_curTrec == null) { + return; + } + _quickList.clearSelection(); + DefaultListModel model = + (DefaultListModel) _quickList.getModel(); + int olddex = model.indexOf(_curTrec); + if (olddex != -1) { + model.set(olddex, ""); + } + model.set(index, _curTrec); + _quickList.setSelectedIndex(index); + + } else { + // select + _quickList.setSelectedIndex(index); + } + } + }); + } + + /** + * Selects the previous tile in the list of available tiles. + */ + public void selectPreviousTile () + { + int row = _tiletable.getSelectedRow(); + if (--row >= 0) { + _tiletable.setRowSelectionInterval(row, row); + } + } + + /** + * Selects the next tile in the list of available tiles. + */ + public void selectNextTile () + { + int row = _tiletable.getSelectedRow(); + if (++row < _tiletable.getRowCount()) { + _tiletable.setRowSelectionInterval(row, row); + } + } + + // documentation inherited + public Dimension getPreferredSize () + { + return new Dimension(WIDTH, HEIGHT); + } + + /** + * Display the tiles in a tileset when it is selected. + */ + public void valueChanged (TreeSelectionEvent e) + { + DefaultMutableTreeNode node = + (DefaultMutableTreeNode) _tsettree.getLastSelectedPathComponent(); + + // we only care when a leaf is selected + if ((node != null) && (node.isLeaf())) { + + _selected = node; + + Object uobj = node.getUserObject(); + if (!(uobj instanceof TileSetRecord)) { + Log.info("Eh? Non-TileSetRecord leaf [obj=" + uobj + + ", class=" + StringUtil.shortClassName(uobj) + "]."); + return; + } + + tileSetSelected((TileSetRecord) uobj); + _quickList.clearSelection(); + } + } + + /** + * Called when a tileset is selected, either via the tree + * or the recent list. + */ + protected void tileSetSelected (TileSetRecord trec) + { + // if they've selected something new, update our tile display + if (_model.getTileSet() != trec.tileSet) { + _curTrec = trec; + _model.setLayerIndex(trec.layer); + + // update the model to reflect new tile set and select tile + // zero by default + _model.setTile(trec.tileSet, trec.tileSetId, 0); + + // update the tile table to reflect the new tileset + updateTileTable(); + +// _quickList.removeListSelectionListener(this); +// // add it to the recent list +// DefaultListModel recentModel = (DefaultListModel) +// _quickList.getModel(); +// recentModel.removeElement(trec); +// recentModel.add(0, trec); +// _quickList.setSelectedIndex(0); +// _quickList.addListSelectionListener(this); + } + } + + /** + * Remove previous test tiles and insert the new batch. + */ + protected void insertTestTiles (HashIntMap tests) + { + // trim the tilesets back to remove any previous test tiles + for (int ii=0; ii < 2; ii++) { + for (int jj=_layerSets[ii].size() - 1; jj >= _layerLengths[ii]; + jj--) { + _layerSets[ii].remove(jj); + } + } + + // insert the new test tiles + Iterator iter = tests.keys(); + while (iter.hasNext()) { + Integer tsid = (Integer) iter.next(); + + TileSet set = (TileSet) tests.get(tsid); + + // determine which layer to which this tileset applies + int lidx = TileSetUtil.getLayerIndex(set); + if (lidx != -1) { + // make up a negative number to refer to this temporary tileset + _layerSets[lidx].add( + new TileSetRecord(lidx, tsid.intValue(), set)); + } + } + + updateTileSetTree(); + } + + /** + * The layer has changed, update the tree to reflect the tilesets + * now available. + */ + public void updateTileSetTree () + { + // first clear out the tree + DefaultTreeModel model = (DefaultTreeModel) _tsettree.getModel(); + DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot(); + root.removeAllChildren(); + + ArrayList expand = new ArrayList(); + + // add all the elements in the base layer + DefaultMutableTreeNode base = new DefaultMutableTreeNode("Base Layer"); + root.add(base); + addNodes(base, getSortedTileSets(EditorModel.BASE_LAYER), + "", 0, expand); + + // add all the elements in the object layer + DefaultMutableTreeNode obj = new DefaultMutableTreeNode("Object Layer"); + root.add(obj); + addNodes(obj, getSortedTileSets(EditorModel.OBJECT_LAYER), + "", 0, expand); + + // notify the JTree that we've put some brand new branches on it. + model.reload(); + + // expand our container categories + for (Iterator iter = expand.iterator(); iter.hasNext(); ) { + _tsettree.expandPath((TreePath)iter.next()); + } + + // now select the previously selected item, or the first... + if (_selected == null) { + _selected = (DefaultMutableTreeNode) root.getFirstLeaf(); + } + _tsettree.setSelectionPath(new TreePath(_selected.getPath())); + } + + /** + * Populate the tree with the available tilesets for the selected layer. + */ + protected TileSetRecord[] getSortedTileSets (int layer) + { + // get the list of tilesets we now want to show + ArrayList sets = _layerSets[layer]; + + // we don't want to sort the actual array since we have + // kept the test tiles at the end + TileSetRecord[] sorted = new TileSetRecord[sets.size()]; + sets.toArray(sorted); + QuickSort.sort(sorted); + + return sorted; + } + + /** + * Recursively add tilesets to the tree. + * + * @param prefix The portion of the full tileset name that we've + * already parsed, it corresponds to the node we're adding to. + * @param position The position in the array from whence to start adding. + * @return the number of elements added to 'node' from 'list'. + */ + protected int addNodes (DefaultMutableTreeNode node, + TileSetRecord[] list, String prefix, int position, + ArrayList expand) + { + int prefixlen = prefix.length(); + + for (int ii = position; ii < list.length; ) { + String name = list[ii].fullname(); + + // if the next name on the list doesn't start with the prefix, + // we have no business adding it to this node. + if (!name.startsWith(prefix)) { + return ii - position; + } + + // is there another category name? + int dex = name.indexOf('/', prefixlen); + if (dex == -1) { + // nope, just add this item to the node. + DefaultMutableTreeNode item = new DefaultMutableTreeNode( + list[ii]); + node.add(item); + + // oh, we're so sneaky! + // if the item we're adding has the same TileSetRecord + // as the previously selected item, we're going to want to + // select it.. + if ((_selected != null) && + (list[ii].equals(_selected.getUserObject()))) { + _selected = item; + } + + ii++; + + } else { + // new category! + String catname = name.substring(prefixlen, dex); + DefaultMutableTreeNode category = + new DefaultMutableTreeNode(catname); + node.add(category); + + // if we have further categories below, start expanded + if (name.indexOf('/', dex+1) != -1) { + expand.add(new TreePath(category.getPath())); + } + + // recurse.. + ii += addNodes(category, list, name.substring(0, dex + 1), + ii, expand); + } + } + + return list.length - position; + } + + /** + * Update the tile table to reflect the currently selected tile set. + */ + protected void updateTileTable () + { + // get the table width before we update the table model since + // updating the model seems to reset the table width to an + // incorrect default + TableColumn tcol = _tiletable.getColumnModel().getColumn(0); + _tablewid = tcol.getWidth() - (2 * EDGE_TILE_H); + + // clear out the old selection because we're going to change + // tilesets + _tiletable.clearSelection(); + + // update the table model with the new tile set tiles + _tablemodel.updateTileSet(); + + // if there are no tiles in the current tile set, we're done + if (!_model.isTileValid()) { + return; + } + + // set row heights to match the scaled tile image heights + int numTiles = getTileCount(); + TileSet set = _model.getTileSet(); + for (int ii = 0; ii < numTiles; ii++) { + Image img = set.getRawTileImage(ii); + int hei = getScaledTileImageHeight(img); + _tiletable.setRowHeight(ii, hei + (2 * EDGE_TILE_V)); + } + + // select the selected tile + int tid = _model.getTileId(); + _tiletable.setRowSelectionInterval(tid, tid); + + if (_scroller != null) { + // scroll to the selected tile + Rectangle r = _tiletable.getCellRect(tid, 0, true); + _scroller.getViewport().setViewPosition(new Point(r.x, r.y)); + } + } + + /** + * Handle tile table selections. + */ + public void valueChanged (ListSelectionEvent e) + { + // ignore extra messages + if (e.getValueIsAdjusting()) { + return; + } + + Object src = e.getSource(); + if (src == _quickList) { + Object o = _quickList.getSelectedValue(); + if (o instanceof TileSetRecord) { + tileSetSelected((TileSetRecord) o); + if (o != _selected.getUserObject()) { + _tsettree.clearSelection(); + } + } + } else { + // otherwise they clicked on the tile table. + ListSelectionModel lsm = (ListSelectionModel) src; + if (!lsm.isSelectionEmpty()) { + _model.setTileId(lsm.getMinSelectionIndex()); + } + } + } + + /** + * Returns the number of tiles in the currently selected tileset. + */ + protected int getTileCount () + { + if (!_model.isTileValid()) { + return 0; + + } else { + TileSet set = _model.getTileSet(); + return (set == null) ? 0 : set.getTileCount(); + } + } + + /** + * Returns the height of the given tile image after scaling to fit + * within the width of the tile table. + */ + protected int getScaledTileImageHeight (Image img) + { + int wid = img.getWidth(null), hei = img.getHeight(null); + if (wid > _tablewid) { + float frac = (float)wid / (float)_tablewid; + return (int)(hei / frac); + } + + return hei; + } + + /** + * Extends the {@link AbstractTableModel} to encapsulate the table + * layout and display options required when displaying the tiles in + * the currently selected tileset. + */ + protected class TileTableModel extends AbstractTableModel + { + /** + * Called when the tile set associated with the table has been + * changed. Clears the cached image icons used to display each + * cell and updates the number of rows in the table to properly + * deal with tile sets of varying sizes. + */ + public synchronized void updateTileSet () + { + int numTiles = getTileCount(); + _icons = new ImageIcon[numTiles]; + fireTableRowsInserted(0, numTiles); + } + + // documentation inherited + public int getColumnCount () + { + return 1; + } + + // documentation inherited + public String getColumnName (int columnIndex) + { + return null; + } + + // documentation inherited + public int getRowCount () + { + return getTileCount(); + } + + // documentation inherited + public Object getValueAt (int row, int col) + { + // return the icon immediately if it's already cached + if (_icons[row] != null) { + return _icons[row]; + } + + // generate and save off the tile image scaled to fit the table + TileSet set = _model.getTileSet(); + Image img = set.getRawTileImage(row); + int hei = getScaledTileImageHeight(img); + + if (hei != img.getHeight(null)) { + img = img.getScaledInstance( + _tablewid, hei, Image.SCALE_SMOOTH); + } + + return (_icons[row] = new ImageIcon(img)); + } + + // documentation inherited + public Class getColumnClass (int c) + { + // return the object associated with the column to force + // rendering of our icon images rather than straight text + return getValueAt(0, c).getClass(); + } + + /** The image icons used to display the table cell contents. */ + protected ImageIcon _icons[]; + } + + /** + * Used to manage tilesets in the tileset selection combobox. + */ + protected static class TileSetRecord implements Comparable + { + public int layer; + public int tileSetId; + public TileSet tileSet; + public String shortname; + + public TileSetRecord (int layer, int tileSetId, TileSet tileSet) + { + this.layer = layer; + this.tileSetId = tileSetId; + this.tileSet = tileSet; + + shortname = fullname(); + + // cut everything before the last slash for our shortname + int lastdex = shortname.lastIndexOf('/'); + if (lastdex != -1) { + shortname = shortname.substring(lastdex + 1); + } + } + + public String fullname () + { + return tileSet.getName(); + } + + public String toString () + { + return shortname; + } + + public int compareTo (Object o) + { + return fullname().compareToIgnoreCase( + ((TileSetRecord) o).fullname()); + } + + public boolean equals (Object o) + { + if (o instanceof TileSetRecord) { + TileSetRecord tsr = (TileSetRecord) o; + return ((tsr.layer == layer) && (tsr.tileSetId == tileSetId)); + } + return false; + } + } + + /** Default desired panel dimensions. */ + protected static final int WIDTH = 400; + protected static final int HEIGHT = 300; + + /** Buffer space surrounding each tile in the tile table. */ + protected static final int EDGE_TILE_H = 4; + protected static final int EDGE_TILE_V = 4; + + /** An ArrayList of TileSetRecords for each layer. */ + protected ArrayList[] _layerSets; + + /** The original number of TileSetRecords for each layer. */ + protected int[] _layerLengths; + + /** The tree listing available tilesets. */ + protected JTree _tsettree; + + /** The selected tree node. */ + protected DefaultMutableTreeNode _selected; + + /** The table listing all tiles in the selected tileset. */ + protected JTable _tiletable; + + /** The list of quickly-selectable tilesets. */ + protected JList _quickList; + + /** The currently selected tileset record. */ + protected TileSetRecord _curTrec; + + /** The width of the tile table column in pixels. */ + protected int _tablewid; + + /** The scroll pane containing the tile table. */ + protected SafeScrollPane _scroller; + + /** The editor model. */ + protected EditorModel _model; + + /** The tile table data model. */ + protected TileTableModel _tablemodel; +} diff --git a/src/java/com/threerings/stage/tools/editor/util/EditorContext.java b/src/java/com/threerings/stage/tools/editor/util/EditorContext.java new file mode 100644 index 000000000..e19de94f3 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/util/EditorContext.java @@ -0,0 +1,31 @@ +// +// $Id: EditorContext.java 7429 2003-04-01 02:19:34Z mdb $ + +package com.threerings.stage.tools.editor.util; + +import java.util.List; + +import com.threerings.media.image.ColorPository; +import com.threerings.media.tile.TileSetRepository; + +import com.threerings.stage.util.StageContext; + +public interface EditorContext extends StageContext +{ + /** + * Return a reference to the tile set repository in use by the tile + * manager. This reference is valid for the lifetime of the + * application. + */ + public TileSetRepository getTileSetRepository (); + + /** + * Returns a colorization repository for use by the editor. + */ + public ColorPository getColorPository (); + + /** + * Inserts all known scene types into the supplied list. + */ + public void enumerateSceneTypes (List types); +} diff --git a/src/java/com/threerings/stage/tools/editor/util/EditorDialogUtil.java b/src/java/com/threerings/stage/tools/editor/util/EditorDialogUtil.java new file mode 100644 index 000000000..beb35d43a --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/util/EditorDialogUtil.java @@ -0,0 +1,100 @@ +// +// $Id: EditorDialogUtil.java 9223 2003-05-27 02:35:00Z mdb $ + +package com.threerings.stage.tools.editor.util; + +import java.awt.Container; +import java.awt.Dimension; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JInternalFrame; +import javax.swing.JLayeredPane; + +import com.threerings.util.DirectionUtil; + +public class EditorDialogUtil +{ + /** + * Add a button to a container with the given parameters and + * action listener. + * + * @param l the listener. + * @param container the container. + * @param name the button name. + * @param cmd the action command. + */ + public static void addButton (ActionListener l, Container container, + String name, String cmd) + { + JButton button = new JButton(name); + button.addActionListener(l); + button.setActionCommand(cmd); + container.add(button); + } + + /** + * Create and return a combo box seeded with the various possible + * orientation direction names. + * + * @param l the listener. + * + * @return the combo box. + */ + public static JComboBox getOrientationComboBox (ActionListener l) + { + JComboBox box = new JComboBox(DirectionUtil.getDirectionNames()); + box.addActionListener(l); + box.setActionCommand("orient"); + return box; + } + + /** + * Centers the supplied dialog in its parent's bounds. + */ + public static void center (JFrame parent, JInternalFrame dialog) + { + Dimension psize = parent.getSize(); + Dimension dsize = dialog.getSize(); + dialog.setLocation((psize.width-dsize.width)/2, + (psize.height-dsize.height)/2); + } + + /** + * Display a dialog centered within the given frame. + * + * @param parent the parent frame. + * @param dialog the dialog. + */ + public static void display (JFrame parent, JInternalFrame dialog) + { + center(parent, dialog); + parent.getLayeredPane().add(dialog, JLayeredPane.POPUP_LAYER); + dialog.setVisible(true); + } + + /** + * Removes the supplied dialog from its parent container, but does not + * dispose it. + */ + public static void dismiss (JInternalFrame dialog) + { + Container parent = dialog.getParent(); + if (parent != null) { + parent.remove(dialog); + parent.repaint(); + } + dialog.setVisible(false); + } + + /** + * Handles safely dismissing and disposing of the supplied dialog. + */ + public static void dispose (JInternalFrame dialog) + { + dismiss(dialog); + dialog.dispose(); + } +} diff --git a/src/java/com/threerings/stage/tools/editor/util/ExtrasPainter.java b/src/java/com/threerings/stage/tools/editor/util/ExtrasPainter.java new file mode 100644 index 000000000..ffcc4eb35 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/util/ExtrasPainter.java @@ -0,0 +1,14 @@ +// +// $Id: ExtrasPainter.java 7998 2003-04-18 18:34:41Z mdb $ + +package com.threerings.stage.tools.editor.util; + +import java.awt.Graphics2D; + +/** + * An interface for painting extra stuff. + */ +public interface ExtrasPainter +{ + public void paintExtras (Graphics2D gfx); +} diff --git a/src/java/com/threerings/stage/tools/editor/util/TileSetUtil.java b/src/java/com/threerings/stage/tools/editor/util/TileSetUtil.java new file mode 100644 index 000000000..bafe1a327 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/util/TileSetUtil.java @@ -0,0 +1,30 @@ +// +// $Id: TileSetUtil.java 9371 2003-06-02 20:28:21Z mdb $ + +package com.threerings.stage.tools.editor.util; + +import com.threerings.media.tile.TileSet; +import com.threerings.miso.tile.BaseTileSet; + +import com.threerings.stage.tools.editor.Log; +import com.threerings.stage.tools.editor.EditorModel; + +/** + * Miscellaneous useful routines for working with lists of {@link TileSet} + * and {@link BaseTileSet} objects. + */ +public class TileSetUtil +{ + /** + * Returns the layer index of the layer for which this tileset + * provides tiles. + */ + public static int getLayerIndex (TileSet set) + { + if (set instanceof BaseTileSet) { + return EditorModel.BASE_LAYER; + } else { + return EditorModel.OBJECT_LAYER; + } + } +} diff --git a/src/java/com/threerings/stage/tools/xml/StageMisoSceneRuleSet.java b/src/java/com/threerings/stage/tools/xml/StageMisoSceneRuleSet.java new file mode 100644 index 000000000..8c3c0c0ee --- /dev/null +++ b/src/java/com/threerings/stage/tools/xml/StageMisoSceneRuleSet.java @@ -0,0 +1,21 @@ +// +// $Id: StageMisoSceneRuleSet.java 20143 2005-03-30 01:12:48Z mdb $ + +package com.threerings.stage.tools.xml; + +import com.threerings.miso.data.SparseMisoSceneModel; +import com.threerings.miso.tools.xml.SparseMisoSceneRuleSet; + +import com.threerings.stage.data.StageMisoSceneModel; + +/** + * Customizes the miso scene rule set such that it creates {@link + * StageMisoSceneModel} instances. + */ +public class StageMisoSceneRuleSet extends SparseMisoSceneRuleSet +{ + protected SparseMisoSceneModel createMisoSceneModel () + { + return new StageMisoSceneModel(); + } +} diff --git a/src/java/com/threerings/stage/tools/xml/StageSceneParser.java b/src/java/com/threerings/stage/tools/xml/StageSceneParser.java new file mode 100644 index 000000000..7a3936286 --- /dev/null +++ b/src/java/com/threerings/stage/tools/xml/StageSceneParser.java @@ -0,0 +1,67 @@ +// +// $Id: StageSceneParser.java 16546 2004-07-27 20:49:56Z ray $ + +package com.threerings.stage.tools.xml; + +import org.apache.commons.digester.Rule; + +import org.xml.sax.Attributes; + +import com.threerings.whirled.spot.tools.xml.SpotSceneRuleSet; +import com.threerings.whirled.tools.xml.SceneParser; +import com.threerings.whirled.tools.xml.SceneRuleSet; + +import com.threerings.stage.data.StageSceneModel; + +/** + * Parses {@link StageSceneModel} instances from an XML description file. + */ +public class StageSceneParser extends SceneParser +{ + /** + * Constructs a parser that can be used to parse Stage scene models. + */ + public StageSceneParser () + { + super(""); + + // add a rule to parse scene colorizations + _digester.addRule("scene/zations/zation", new Rule() { + public void begin (String namespace, String name, + Attributes attrs) throws Exception { + StageSceneModel yoscene = (StageSceneModel) digester.peek(); + int classId = Integer.parseInt(attrs.getValue("classId")); + int colorId = Integer.parseInt(attrs.getValue("colorId")); + yoscene.setDefaultColor(classId, colorId); + } + }); + + // add rule sets for our aux scene models + registerAuxRuleSet(new SpotSceneRuleSet()); + registerAuxRuleSet(new StageMisoSceneRuleSet()); + } + + // documentation inherited from interface + protected SceneRuleSet createSceneRuleSet () + { + return new StageSceneRuleSet(); + } + + /** + * A simple hook for parsing a single scene from the command line. + */ + public static void main (String[] args) + { + if (args.length < 1) { + System.err.println("Usage: StageSceneParser scene.xml"); + System.exit(-1); + } + + try { + System.out.println( + "Parsed " + new StageSceneParser().parseScene(args[0])); + } catch (Exception e) { + e.printStackTrace(System.err); + } + } +} diff --git a/src/java/com/threerings/stage/tools/xml/StageSceneRuleSet.java b/src/java/com/threerings/stage/tools/xml/StageSceneRuleSet.java new file mode 100644 index 000000000..965cd7e81 --- /dev/null +++ b/src/java/com/threerings/stage/tools/xml/StageSceneRuleSet.java @@ -0,0 +1,21 @@ +// +// $Id: StageSceneRuleSet.java 6370 2003-02-12 07:32:00Z mdb $ + +package com.threerings.stage.tools.xml; + +import com.threerings.whirled.tools.xml.SceneRuleSet; + +import com.threerings.stage.data.StageScene; +import com.threerings.stage.data.StageSceneModel; + +/** + * Used to parse an {@link StageScene} from XML. + */ +public class StageSceneRuleSet extends SceneRuleSet +{ + // documentation inherited + protected Class getSceneClass () + { + return StageSceneModel.class; + } +} diff --git a/src/java/com/threerings/stage/tools/xml/StageSceneWriter.java b/src/java/com/threerings/stage/tools/xml/StageSceneWriter.java new file mode 100644 index 000000000..be74e5347 --- /dev/null +++ b/src/java/com/threerings/stage/tools/xml/StageSceneWriter.java @@ -0,0 +1,67 @@ +// +// $Id: StageSceneWriter.java 20143 2005-03-30 01:12:48Z mdb $ + +package com.threerings.stage.tools.xml; + +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import com.megginson.sax.DataWriter; + +import com.threerings.miso.tools.xml.SparseMisoSceneWriter; + +import com.threerings.whirled.spot.data.SpotSceneModel; +import com.threerings.whirled.spot.tools.xml.SpotSceneWriter; + +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.tools.xml.SceneWriter; + +import com.threerings.stage.data.StageMisoSceneModel; + +import com.threerings.stage.data.StageSceneModel; + +/** + * Generates an XML representation of a {@link StageSceneModel}. + */ +public class StageSceneWriter extends SceneWriter +{ + public StageSceneWriter () + { + // register our auxiliary model writers + registerAuxWriter(SpotSceneModel.class, new SpotSceneWriter()); + registerAuxWriter(StageMisoSceneModel.class, + new SparseMisoSceneWriter()); + } + + // documentation inherited + protected void addSceneAttributes (SceneModel scene, AttributesImpl attrs) + { + super.addSceneAttributes(scene, attrs); + StageSceneModel sscene = (StageSceneModel)scene; + attrs.addAttribute("", "type", "", "", sscene.type); + } + + // documentation inherited + protected void writeSceneData (SceneModel scene, DataWriter writer) + throws SAXException + { + // write out any default colorizations + StageSceneModel sscene = (StageSceneModel)scene; + if (sscene.defaultColors != null) { + writer.startElement("zations"); + int[] keys = sscene.defaultColors.getKeys(); + for (int ii=0, nn=keys.length; ii < nn; ii++) { + int value = sscene.defaultColors.get(keys[ii]); + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute("", "classId", "", "", + String.valueOf(keys[ii])); + attrs.addAttribute("", "colorId", "", "", + String.valueOf(value)); + writer.emptyElement("", "zation", "", attrs); + } + writer.endElement("zations"); + } + + super.writeSceneData(scene, writer); + } +} diff --git a/tests/rsrc/config/resource/editor.properties b/tests/rsrc/config/resource/editor.properties new file mode 100644 index 000000000..55eaa2631 --- /dev/null +++ b/tests/rsrc/config/resource/editor.properties @@ -0,0 +1,16 @@ +# +# $Id: manager.properties 2121 2003-01-13 22:55:28Z mdb $ +# +# Test resource manager configuration + +# configure the BundledTileSetRepository +resource.set.tilesets = bundles/tiles/ground/bundle.jar:\ + bundles/tiles/objects/bundle.jar + +# configure the BundledComponentRepository +resource.set.components = bundles/components/metadata.jar:\ + bundles/components/pirate/components.jar: \ + bundles/components/vessel/components.jar + +resource.set.general = \ + bundles/components/pirate/components.jar diff --git a/tests/rsrc/config/stage/iconmgr.properties b/tests/rsrc/config/stage/iconmgr.properties new file mode 100644 index 000000000..56d08dc90 --- /dev/null +++ b/tests/rsrc/config/stage/iconmgr.properties @@ -0,0 +1,103 @@ +# +# $Id: iconmgr.properties 20012 2005-03-24 23:19:41Z ray $ +# +# Configuration for the icon manager + +# simple arrow images in the order: down, up, right, left +arrows.path = media/yohoho/icons/arrows.png +arrows.metrics = 16, 16 + +# simpler arrows used to change values +valuearrows.path = media/yohoho/icons/value_arrows.png +valuearrows.metrics = 16, 15 + +# up and down arrows with "+" and "-" in them +zoomarrows.path = media/yohoho/icons/zoom_arrows.png +zoomarrows.metrics = 24, 14 + +# collapsible entry plus and minus icons +collapse.path = media/yohoho/icons/collapse.png +collapse.metrics = 8, 8 + +# icons for the tabbed panes +tabtitles.path = media/yohoho/icons/tabtitles.png +tabtitles.metrics = 16, 16 + +# icons for crew ranks +ranks.path = media/yohoho/icons/crew_ranks.png +ranks.metrics = 25, 25 + +# icon for mateys +matey.path = media/yohoho/icons/matey_icon.png +matey.metrics = 25, 25 + +# icons for radial menus +radmenu.path = media/yohoho/icons/radial.png +radmenu.metrics = 32, 32 + +# icons for portal traversal +portal.path = media/yohoho/icons/portal_arrows.png +portal.metrics = 48, 48 + +# icons for youser activities +activity.path = media/yohoho/icons/activity.png +activity.metrics = 27, 27 + +# icons for youser activities +puzzles.path = media/yohoho/icons/puzzles.png +puzzles.metrics = 24, 24 + +# icons for pirate info icons +pirateinfo.path = media/yohoho/icons/pirate_info.png +pirateinfo.metrics = 28, 28 + +# icons for scrolly history +historyscroll.path = media/yohoho/icons/history-scroll.png +historyscroll.metrics = 14, 11 + +# icons for full-screen history +historyfull.path = media/yohoho/icons/history-full.png +historyfull.metrics = 14, 11 + +# icons for the duty status button +dutystatus.path = media/yohoho/icons/duty-status.png +dutystatus.metrics = 16, 12 + +hiring.path = media/yohoho/icons/hiring.png +hiring.metrics = 17, 17 + +# icons for the wee lock images +lock.path = media/yohoho/icons/lock.png +lock.metrics = 23, 18 + +# icon for the ferry +ferry.path = media/yohoho/icons/ferry.png +ferry.metrics = 58, 48 + +# icon for whisking +whisk.path = media/yohoho/icons/whisk.png +whisk.metrics = 12, 11 + +# icon to indicate that a player was present for a sea battle +sea_battle.path = media/yohoho/icons/sea_battle.png +sea_battle.metrics = 13, 12 + +# item menu icons +item_action.path = media/yohoho/icons/item_actions.png +item_action.metrics = 27, 27 + +# portrait eye menu icons +portrait_eyes_male.path = media/yohoho/icons/portrait_eyes_male.png +portrait_eyes_male.metrics = 70, 25 +portrait_eyes_female.path = media/yohoho/icons/portrait_eyes_female.png +portrait_eyes_female.metrics = 70, 25 + +# portrait mouth menu icons +portrait_mouths_male.path = media/yohoho/icons/portrait_mouths_male.png +portrait_mouths_male.metrics = 70, 25 +portrait_mouths_female.path = media/yohoho/icons/portrait_mouths_female.png +portrait_mouths_female.metrics = 70, 25 + +# dnd icon +dnd.path = media/yohoho/icons/dnd.png +dnd.metrics = 32, 32