package rabbitescape.ui.swing; import static rabbitescape.engine.i18n.Translation.t; import static rabbitescape.ui.swing.SwingConfigSetup.*; import java.awt.BorderLayout; import java.awt.Canvas; import java.awt.Color; import java.awt.Container; import java.awt.Dimension; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.ComponentEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.awt.event.WindowEvent; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.KeyStroke; import rabbitescape.engine.Token; import rabbitescape.engine.config.Config; import rabbitescape.engine.config.ConfigTools; import rabbitescape.engine.config.TapTimer; import rabbitescape.engine.solution.SelectAction; import rabbitescape.render.BitmapCache; import rabbitescape.render.gameloop.Physics.StatsChangedListener; public class GameUi implements StatsChangedListener { private class Listener extends EmptyListener implements MouseWheelListener { private int startX = -1; private int startY = -1; private long msTimePress = 0; /** Time in ms. Longer press-release intervals are interpreted as drags */ private final long msClickThreshold = ConfigTools.getInt( uiConfig, CFG_CLICK_THRESHOLD_MS ); @Override public void windowClosing( WindowEvent e ) { stopGameLoop(); } @Override public void componentResized( ComponentEvent e ) { zoomToFit(); } @Override public void mousePressed( MouseEvent e ) { TapTimer.newTap(); if ( noScrollRequired() ) { click( e.getPoint() ); return; } msTimePress = System.currentTimeMillis(); startX = e.getX(); startY = e.getY(); } @Override public void mouseReleased( MouseEvent e ) { long msDownTime = System.currentTimeMillis() - msTimePress; if ( msDownTime < msClickThreshold ) { click( e.getPoint() ); } } @Override public void mouseClicked( MouseEvent e ) { // use pressed and released calls. // if this was used too, would get double event calls. } @Override public void mouseDragged( MouseEvent e ) { long msDownTime = System.currentTimeMillis() - msTimePress; if ( msDownTime < msClickThreshold ) { // Wait and see if this is a click or a drag return; } canvasScrollBarX.setValue( canvasScrollBarX.getValue() + startX - e.getX() ); canvasScrollBarY.setValue( canvasScrollBarY.getValue() + startY - e.getY() ); startX = e.getX(); startY = e.getY(); } /** * @return true if the window is large such that the whole world is * visible. */ public boolean noScrollRequired() { int xRange = canvasScrollBarX.getMaximum() - canvasScrollBarX.getMinimum(); int yRange = canvasScrollBarY.getMaximum() - canvasScrollBarY.getMinimum(); return ( canvasScrollBarX.getVisibleAmount() == xRange ) && ( canvasScrollBarY.getVisibleAmount() == yRange ); } @Override public void mouseWheelMoved( MouseWheelEvent e ) { if ( e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL ) { if ( e.isControlDown() ) { zoomClicked( e.getUnitsToScroll() < 0 ); } else { int units = e.getUnitsToScroll(); if ( canvasScrollBarY.getVisibleAmount() != canvasScrollBarY.getMaximum() ) { scrollScrollBarBy( canvasScrollBarY, units ); } else { scrollScrollBarBy( canvasScrollBarX, units ); } } } } private void scrollScrollBarBy( JScrollBar scrollBar, int units ) { scrollBar.setValue( scrollBar.getValue() + ( units * scrollBar.getUnitIncrement() / 2 ) ); } @Override public void keyPressed( KeyEvent e ) { switch ( e.getKeyCode() ) { case KeyEvent.VK_LEFT: { scrollScrollBarBy( canvasScrollBarX, -1 ); break; } case KeyEvent.VK_RIGHT: { scrollScrollBarBy( canvasScrollBarX, 1 ); break; } case KeyEvent.VK_UP: { scrollScrollBarBy( canvasScrollBarY, -1 ); break; } case KeyEvent.VK_DOWN: { scrollScrollBarBy( canvasScrollBarY, 1 ); break; } } } } private static final Color backgroundColor = Color.WHITE; private static int[] zoomValues = { 16, 24, 32, 48, 64, 96, 128 }; // 32x32 is the lowest "reasonable" zoom size private static int MIN_AUTO_ZOOM_INDEX = 2; private final Container contentPane; private final JPanel middlePanel; private final Dimension buttonSizeInPixels; public Dimension worldSizeInPixels; private int worldTileSizeInPixels; private final Config uiConfig; private final BitmapCache<SwingBitmap> bitmapCache; private final MainJFrame frame; private final MenuUi menuUi; public final Canvas canvas; private Listener listener; private JScrollBar canvasScrollBarX; private JScrollBar canvasScrollBarY; private GameMenu menu; private TopBar topBar; private Token.Type chosenAbility; private SwingGameLaunch gameLaunch; // Modified in Swing event thread, read in game loop thread public int scrollX; public int scrollY; public int zoomIndex; public GameUi( Config uiConfig, BitmapCache<SwingBitmap> bitmapCache, MainJFrame frame, MenuUi menuUi ) { this.uiConfig = uiConfig; this.bitmapCache = bitmapCache; this.frame = frame; this.menuUi = menuUi; this.contentPane = frame.getContentPane(); this.middlePanel = new JPanel( new BorderLayout() ); this.chosenAbility = null; this.gameLaunch = null; this.buttonSizeInPixels = new Dimension( 32, 32 ); this.zoomIndex = 2; this.worldTileSizeInPixels = zoomValues[zoomIndex]; this.worldSizeInPixels = new Dimension( 400, 200 ); // Temporary guess this.scrollX = 0; this.scrollY = 0; this.canvas = initUi(); adjustScrollBars(); this.menu = null; this.topBar = null; } private Canvas initUi() { contentPane.add( middlePanel, BorderLayout.CENTER ); Canvas canvas = initCanvas( middlePanel, worldSizeInPixels ); frame.setBoundsFromConfig(); frame.setTitle( t( "Rabbit Escape" ) ); frame.pack(); frame.setVisible( true ); // Must do this after frame is made visible canvas.createBufferStrategy( 2 ); return canvas; } private Canvas initCanvas( Container contentPane, Dimension worldSizeInPixels ) { Canvas canvas = new Canvas(); canvas.setIgnoreRepaint( true ); canvasScrollBarX = new JScrollBar( JScrollBar.HORIZONTAL ); canvasScrollBarY = new JScrollBar( JScrollBar.VERTICAL ); JPanel canvasPanel = new JPanel( new BorderLayout() ); canvasPanel.add( canvas, BorderLayout.CENTER ); canvasPanel.add( canvasScrollBarX, BorderLayout.SOUTH ); canvasPanel.add( canvasScrollBarY, BorderLayout.EAST ); contentPane.add( canvasPanel, BorderLayout.CENTER ); return canvas; } private void zoomToFit() { // Start at MIN_AUTO_ZOOM_INDEX + 1 to enforce at least 32x32 for ( int index = MIN_AUTO_ZOOM_INDEX + 1; index < zoomValues.length; ++index ) { if ( zoomIndexTooBig( index ) ) { zoomTo( index - 1 ); return; } } zoomTo( zoomValues.length - 1 ); } private boolean zoomIndexTooBig( int index ) { int zoom = zoomValues[index]; return ( zoom * gameLaunch.world.size.width > canvas.getWidth() || zoom * gameLaunch.world.size.height > canvas.getHeight() ); } private void adjustScrollBars() { canvasScrollBarX.setMaximum( worldSizeInPixels.width ); canvasScrollBarX.setVisibleAmount( canvas.getWidth() ); canvasScrollBarX.setBlockIncrement( (int)( canvas.getWidth() * 0.9 ) ); canvasScrollBarX.setUnitIncrement( worldTileSizeInPixels ); canvasScrollBarY.setMaximum( worldSizeInPixels.height ); canvasScrollBarY.setVisibleAmount( canvas.getHeight() ); canvasScrollBarY.setBlockIncrement( (int)( canvas.getHeight() * 0.9 ) ); canvasScrollBarY.setUnitIncrement( worldTileSizeInPixels ); } private void initListeners() { listener = new Listener(); canvas.addKeyListener( listener ); canvas.addMouseListener( listener ); canvas.addMouseWheelListener( listener ); canvas.addMouseMotionListener( listener ); frame.addComponentListener( listener ); frame.addWindowListener( listener ); frame.addKeyListener( listener ); gameLaunch.addStatsChangedListener( this.topBar ); gameLaunch.addStatsChangedListener( this ); menu.addAbilitiesListener( new GameMenu.AbilityChangedListener() { @Override public void abilityChosen( Token.Type ability ) { chooseAbility( ability ); gameLaunch.solutionRecorder.append( new SelectAction( ability ) ); } } ); menu.back.addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent evt ) { exit(); } } ); MenuTools.clickOnKey( menu.back, "back", KeyEvent.VK_ESCAPE ); menu.explodeAll.addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent evt ) { explodeAllClicked(); } } ); MenuTools.clickOnKey( menu.explodeAll, "explode_all", KeyEvent.VK_X ); menu.zoomIn.addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent evt ) { zoomClicked( true ); } } ); MenuTools.clickOnKey( menu.zoomIn, "zoom_in", KeyStroke.getKeyStroke( KeyEvent.VK_EQUALS, 0 ), KeyStroke.getKeyStroke( KeyEvent.VK_EQUALS, java.awt.event.InputEvent.SHIFT_DOWN_MASK ), KeyStroke.getKeyStroke( KeyEvent.VK_EQUALS, java.awt.event.InputEvent.CTRL_DOWN_MASK ), KeyStroke.getKeyStroke( KeyEvent.VK_EQUALS, java.awt.event.InputEvent.CTRL_DOWN_MASK | java.awt.event.InputEvent.SHIFT_DOWN_MASK ) ); menu.zoomOut.addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent evt ) { zoomClicked( false ); } } ); MenuTools.clickOnKey( menu.zoomOut, "zoom_out", KeyStroke.getKeyStroke( KeyEvent.VK_MINUS, 0 ), KeyStroke.getKeyStroke( KeyEvent.VK_MINUS, java.awt.event.InputEvent.CTRL_DOWN_MASK ) ); menu.mute.addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent evt ) { setMuted( menu.mute.isSelected() ); } } ); MenuTools.clickOnKey( menu.mute, "mute", KeyEvent.VK_M ); menu.pause.addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent evt ) { gameLaunch.world.setPaused( menu.pause.isSelected() ); } } ); MenuTools.clickOnKey( menu.pause, "pause", KeyEvent.VK_P ); menu.speed.addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent evt ) { gameLaunch.toggleSpeed(); } } ); MenuTools.clickOnKey( menu.speed, "speed up", KeyEvent.VK_S ); canvasScrollBarX.addAdjustmentListener( new AdjustmentListener() { @Override public void adjustmentValueChanged( AdjustmentEvent e ) { scrollX = e.getValue(); } } ); canvasScrollBarY.addAdjustmentListener( new AdjustmentListener() { @Override public void adjustmentValueChanged( AdjustmentEvent e ) { scrollY = e.getValue(); } } ); } public void setGameLaunch( SwingGameLaunch gameLaunch ) { this.gameLaunch = gameLaunch; this.menu = new GameMenu( contentPane, bitmapCache, buttonSizeInPixels, uiConfig, backgroundColor, gameLaunch.getAbilities() ); this.topBar = new TopBar( backgroundColor, gameLaunch.world.num_to_save, middlePanel, gameLaunch.world.name ); frame.pack(); initListeners(); zoomToFit(); } public void setWorldSize( rabbitescape.engine.util.Dimension worldGridSize, int worldTileSizeInPixels ) { this.worldSizeInPixels = new Dimension( worldGridSize.width * worldTileSizeInPixels, worldGridSize.height * worldTileSizeInPixels ); this.worldTileSizeInPixels = worldTileSizeInPixels; canvas.setPreferredSize( worldSizeInPixels ); adjustScrollBars(); } public void exit() { stopGameLoop(); uninit(); if ( menuUi != null ) { menuUi.init(); } } private void stopGameLoop() { if ( gameLaunch != null ) { gameLaunch.stop(); gameLaunch.world.setPaused( false ); } } private void uninit() { frame.getContentPane().removeAll(); frame.removeComponentListener( listener ); frame.removeWindowListener( listener ); frame.removeKeyListener( listener ); } private void explodeAllClicked() { switch ( gameLaunch.world.completionState() ) { case RUNNING: case PAUSED: { gameLaunch.checkExplodeAll(); break; } default: { // Don't do anything if we've finished already break; } } } private void zoomClicked( boolean zoomIn ) { if ( zoomIn ) { if ( zoomIndex < zoomValues.length - 1 ) { zoomTo( zoomIndex + 1 ); } } else { if ( zoomIndex > 0 ) { zoomTo( zoomIndex - 1 ); } } } private void zoomTo( int zoomIndex ) { this.zoomIndex = zoomIndex; int zoom = zoomValues[zoomIndex]; double scrX = getScrollBarProportion( canvasScrollBarX ); double scrY = getScrollBarProportion( canvasScrollBarY ); gameLaunch.graphics.setTileSize( zoom ); setWorldSize( gameLaunch.world.size, zoom ); setScrollBarFromProportion( canvasScrollBarX, scrX ); setScrollBarFromProportion( canvasScrollBarY, scrY ); } private static double getScrollBarProportion( JScrollBar scrollBar ) { return ( ( scrollBar.getValue() + ( scrollBar.getVisibleAmount() / 2 ) ) / (double)scrollBar.getMaximum() ); } private static void setScrollBarFromProportion( JScrollBar scrollBar, double proportion ) { scrollBar.setValue( (int)( ( scrollBar.getMaximum() * proportion ) - ( scrollBar.getVisibleAmount() / 2 ) ) ); } public boolean getMuted() { return ConfigTools.getBool( uiConfig, CFG_MUTED ); } private void setMuted( boolean muted ) { ConfigTools.setBool( uiConfig, CFG_MUTED, muted ); uiConfig.save(); gameLaunch.graphics.setMuted( muted ); } private Point pixelToCell( Point pixelPosition ) { return new Point( ( pixelPosition.x - gameLaunch.graphics.renderer.offsetX ) / worldTileSizeInPixels, ( pixelPosition.y - gameLaunch.graphics.renderer.offsetY ) / worldTileSizeInPixels ); } private void click( Point pixelPosition ) { Point p = pixelToCell( pixelPosition ); addToken( p.x , p.y ); } protected void addToken(int tileX, int tileY ) { if ( chosenAbility == null ) { return; } int numLeft = gameLaunch.addToken( tileX, tileY, chosenAbility ); if ( numLeft == 0 ) { menu.abilities.get( chosenAbility ).setEnabled( false ); } updateChosenAbility(); } protected void chooseAbility( Token.Type ability ) { chosenAbility = ability; updateChosenAbility(); } private void updateChosenAbility() { topBar.abilityChanged( chosenAbility, gameLaunch.world.abilities.get( chosenAbility ) ); } @Override public void changed( int waiting, int out, int saved ) { switch ( gameLaunch.world.completionState() ) { case WON: case LOST: { gameLaunch.stop(); break; } default: { break; } } } }