package games.strategy.ui; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.InputEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelListener; import java.util.ArrayList; import java.util.List; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import games.strategy.engine.ClientContext; import games.strategy.triplea.settings.scrolling.ScrollSettings; /** * A large image that can be scrolled according to a ImageScrollModel. * Generally used in conjunction with a ImageScrollerSmallView. * We do not take care of drawing ourselves. All we do is keep track of * our location and size. Subclasses must take care of rendering */ public class ImageScrollerLargeView extends JComponent { private static final long serialVersionUID = -7212817233833868483L; // bit flags for determining which way we are scrolling static final int NONE = 0; static final int LEFT = 1; static final int RIGHT = 2; static final int TOP = 4; static final int BOTTOM = 8; private final ScrollSettings scrollSettings; protected final ImageScrollModel m_model; protected double m_scale = 1; private int m_drag_scrolling_lastx; private int m_drag_scrolling_lasty; private boolean wasLastActionDragging = false; public boolean wasLastActionDraggingAndReset() { if (wasLastActionDragging) { wasLastActionDragging = false; return true; } return false; } private final ActionListener m_timerAction = new ActionListener() { @Override public final void actionPerformed(final ActionEvent e) { if (JOptionPane.getFrameForComponent(ImageScrollerLargeView.this).getFocusOwner() == null) { m_insideCount = 0; return; } if (m_inside && m_edge != NONE) { m_insideCount++; if (m_insideCount > 6) { // Scroll the map when the mouse has hovered inside the scroll zone for long enough SwingUtilities.invokeLater(new Scroller()); } } } }; // scrolling private final javax.swing.Timer m_timer = new javax.swing.Timer(50, m_timerAction); private boolean m_inside = false; private int m_insideCount = 0; private int m_edge = NONE; private final List<ScrollListener> m_scrollListeners = new ArrayList<>(); public ImageScrollerLargeView(final Dimension dimension, final ImageScrollModel model) { super(); scrollSettings = ClientContext.scrollSettings(); m_model = model; m_model.setMaxBounds((int) dimension.getWidth(), (int) dimension.getHeight()); setPreferredSize(getImageDimensions()); setMaximumSize(getImageDimensions()); final MouseWheelListener MOUSE_WHEEL_LISTENER = e -> { if (!e.isAltDown()) { if (m_edge == NONE) { m_insideCount = 0; } // compute the amount to move int dx = 0; int dy = 0; if ((e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) == InputEvent.SHIFT_DOWN_MASK) { dx = e.getWheelRotation() * scrollSettings.getWheelScrollAmount(); } else { dy = e.getWheelRotation() * scrollSettings.getWheelScrollAmount(); } // move left and right and test for wrap int newX = (m_model.getX() + dx); if (newX > m_model.getMaxWidth() - getWidth()) { newX -= m_model.getMaxWidth(); } if (newX < -getWidth()) { newX += m_model.getMaxWidth(); } // move up and down and test for edges final int newY = m_model.getY() + dy; // update the map m_model.set(newX, newY); } else { double value = m_scale; int positive = 1; if (e.getUnitsToScroll() > 0) { positive = -1; } if ((positive > 0 && value == 1) || (positive < 0 && value <= .21)) { return; } if (positive > 0) { if (value >= .79) { value = 1.0; } else if (value >= .59) { value = .8; } else if (value >= .39) { value = .6; } else if (value >= .19) { value = .4; } else { value = .2; } } else { if (value <= .41) { value = .2; } else if (value <= .61) { value = .4; } else if (value <= .81) { value = .6; } else if (value <= 1.0) { value = .8; } else { value = 1.0; } } setScale(value); } }; addMouseWheelListener(MOUSE_WHEEL_LISTENER); final MouseAdapter MOUSE_LISTENER = new MouseAdapter() { @Override public void mouseEntered(final MouseEvent e) { m_timer.start(); } @Override public void mouseExited(final MouseEvent e) { m_inside = false; m_timer.stop(); } @Override public void mouseClicked(final MouseEvent e) { requestFocusInWindow(); } @Override public void mouseReleased(final MouseEvent e) { requestFocusInWindow(); } }; addMouseListener(MOUSE_LISTENER); final MouseAdapter MOUSE_LISTENER_DRAG_SCROLLING = new MouseAdapter() { @Override public void mousePressed(final MouseEvent e) { // try to center around the click m_drag_scrolling_lastx = e.getX(); m_drag_scrolling_lasty = e.getY(); } }; addMouseListener(MOUSE_LISTENER_DRAG_SCROLLING); final MouseMotionListener MOUSE_MOTION_LISTENER = new MouseMotionAdapter() { @Override public void mouseMoved(final MouseEvent e) { m_inside = true; final int x = e.getX(); final int y = e.getY(); final int height = getHeight(); final int width = getWidth(); m_edge = getNewEdge(x, y, width, height); if (m_edge == NONE) { m_insideCount = 0; } } }; addMouseMotionListener(MOUSE_MOTION_LISTENER); /* * this is used to detect drag scrolling */ final MouseMotionListener MOUSE_DRAG_LISTENER = new MouseMotionAdapter() { @Override public void mouseDragged(final MouseEvent e) { requestFocusInWindow(); // the right button must be the one down if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0 || (e.getModifiers() & InputEvent.BUTTON2_MASK) != 0) { wasLastActionDragging = true; m_inside = false; // read in location final int x = e.getX(); final int y = e.getY(); if (m_edge == NONE) { m_insideCount = 0; } // compute the amount to move final int dx = (m_drag_scrolling_lastx - x); final int dy = (m_drag_scrolling_lasty - y); // move left and right and test for wrap final int newX = (m_model.getX() + dx); // move up and down and test for edges final int newY = m_model.getY() + dy; // update the map m_model.set(newX, newY); // store the location of the mouse for the next move m_drag_scrolling_lastx = e.getX(); m_drag_scrolling_lasty = e.getY(); } } }; addMouseMotionListener(MOUSE_DRAG_LISTENER); final ComponentListener COMPONENT_LISTENER = new ComponentAdapter() { @Override public void componentResized(final ComponentEvent e) { refreshBoxSize(); } }; addComponentListener(COMPONENT_LISTENER); m_timer.start(); m_model.addObserver((o, arg) -> { repaint(); notifyScollListeners(); }); } /** * For subclasses needing to set the location of the image. */ protected void setTopLeft(final int x, final int y) { m_model.set(x, y); } protected void setTopLeftNoWrap(int x, int y) { if (x < 0) { x = 0; } if (y < 0) { y = 0; } m_model.set(x, y); } public int getImageWidth() { return m_model.getMaxWidth(); } public int getImageHeight() { return m_model.getMaxHeight(); } public void addScrollListener(final ScrollListener s) { m_scrollListeners.add(s); } public void removeScrollListener(final ScrollListener s) { m_scrollListeners.remove(s); } private void notifyScollListeners() { for (final ScrollListener element : new ArrayList<>(m_scrollListeners)) { element.scrolled(m_model.getX(), m_model.getY()); } } private void scroll() { int dy = 0; if ((m_edge & TOP) != 0) { dy = -scrollSettings.getMapEdgeScrollSpeed(); } else if ((m_edge & BOTTOM) != 0) { dy = scrollSettings.getMapEdgeScrollSpeed(); } int dx = 0; if ((m_edge & LEFT) != 0) { dx = -scrollSettings.getMapEdgeScrollSpeed(); } else if ((m_edge & RIGHT) != 0) { dx = scrollSettings.getMapEdgeScrollSpeed(); } dx = (int) (dx / m_scale); dy = (int) (dy / m_scale); final int newX = (m_model.getX() + dx); final int newY = m_model.getY() + dy; m_model.set(newX, newY); } public Dimension getImageDimensions() { return new Dimension(m_model.getMaxWidth(), m_model.getMaxHeight()); } private int getNewEdge(final int x, final int y, final int width, final int height) { int newEdge = NONE; if (x < scrollSettings.getMapEdgeScrollZoneSize()) { newEdge += LEFT; } else if (width - x < scrollSettings.getMapEdgeScrollZoneSize()) { newEdge += RIGHT; } if (y < scrollSettings.getMapEdgeScrollZoneSize()) { newEdge += TOP; } else if (height - y < scrollSettings.getMapEdgeScrollZoneSize()) { newEdge += BOTTOM; } return newEdge; } protected void refreshBoxSize() { m_model.setBoxDimensions((int) (getWidth() / m_scale), (int) (getHeight() / m_scale)); } /** * @param value The new scale value. Constrained to the bounds of no less than 0.15 and no greater than 1. * If out of bounds the nearest boundary value is used. */ public void setScale(double value) { if (value < 0.15) { value = 0.15; } if (value > 1) { value = 1; } // we want the ratio to be a multiple of 1/256 // so that the tiles have integer widths and heights value = ((int) (value * 256)) / ((double) 256); m_scale = value; refreshBoxSize(); } /** * Update will not be seen until update is called. Resets the offscreen * image to the original. */ public int getXOffset() { return m_model.getX(); } public int getYOffset() { return m_model.getY(); } private class Scroller implements Runnable { @Override public void run() { scroll(); } } protected double getScaledWidth() { return getWidth() / m_scale; } protected double getScaledHeight() { return getHeight() / m_scale; } public void deactivate() { m_timer.stop(); m_timer.removeActionListener(m_timerAction); } }