Yehor Boiar
Yehor Boiar

Reputation: 11

Panning using mouse in MouseAdapter and Java swing libraries

I'm trying to implement zooming and panning feature in my implementation of Convay's game of life in java. The problem it the following:

Each time when we try to pan by draging our mouse, our eye sight appears in the left top corner of the screen. Here is the class that responsible for implementing this feature

package input;

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import renderer.Renderer;

public class PanningHandler extends MouseAdapter {
    private Renderer renderer;

    public PanningHandler(Renderer renderer) {
        this.renderer = renderer;
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        // Check if the right mouse button is pressed (RMB)
        if ((e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
            handleRMBDrag(e);
        }
    }

    private void handleRMBDrag(MouseEvent e) {
        int deltaX = e.getX() - renderer.getLastMouseX();
        int deltaY = e.getY() - renderer.getLastMouseY();

        // Calculate new pan offset
        int newPanOffsetX = renderer.getPanOffsetX() - deltaX;
        int newPanOffsetY = renderer.getPanOffsetY() - deltaY;

        // Calculate maximum allowed values based on the size of the display area
        int maxPanOffsetX = getMaxPanOffsetX(); // Adjust this based on your requirements
        int maxPanOffsetY = getMaxPanOffsetY(); // Adjust this based on your requirements

        // Limit the pan offset to avoid going out of bounds
        newPanOffsetX = Math.max(0, Math.min(newPanOffsetX, maxPanOffsetX));
        newPanOffsetY = Math.max(0, Math.min(newPanOffsetY, maxPanOffsetY));

        // Set the new pan offset and repaint
        renderer.setPanOffsetX(newPanOffsetX);
        renderer.setPanOffsetY(newPanOffsetY);
        renderer.getFrame().repaint();

        // Update last mouse coordinates for the next iteration
        renderer.setLastMouseX(e.getX());
        renderer.setLastMouseY(e.getY());
    }

    // Rest of the code remains unchanged

    private int getMaxPanOffsetX() {
        return (int) (renderer.getWidth() * 10 * renderer.getZoomFactor() - renderer.getFrame().getWidth());
    }

    private int getMaxPanOffsetY() {
        return (int) (renderer.getHeight() * 10 * renderer.getZoomFactor() - renderer.getFrame().getHeight());
    }
}

And here is some bits from renderer that are responsible for drawing our squares and panning.

public class Renderer {
    private double zoomFactor = 1.0;
    private boolean gameState = false;
    private final Color BLACK = Color.BLACK;
    private final Color WHITE = Color.WHITE;
    private JFrame frame;
    private static Renderer instance;
    private int height = 100; // TODO - Handle the case when our grid becomes very large (e.g 1000x1000)
    private int width = 100;
    private boolean[][] grid = new boolean[height][width];
    private Logic logic = new Logic(); // instantiate Logic class
    private int lastMouseX = -1;
    private int lastMouseY = -1;
    private int panOffsetX = 0;
    private int panOffsetY = 0;

    private void configFrame() {
        frame.pack();
        frame.setSize(width * 10, height * 10);
        frame.getContentPane().setBackground(BLACK);

        // ...
        frame.addMouseMotionListener(new PanningHandler(this)); // TODO - make it work
        // ...

        frame.add(new MyPanel());
        frame.setResizable(false);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

    /**
     * Inner class representing the drawing panel inside the JFrame.
     */
    class MyPanel extends JPanel {
        public MyPanel() {
            setBackground(BLACK); // Set the background color to black
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            drawSquares(g);
        }
    }

    /**
     * Draws squares on the panel based on the current state of the grid.
     * 
     * @param g The Graphics object used for drawing.
     */
    public void drawSquares(Graphics g) {
        int squareSize = (int) (10 * zoomFactor);

        for (int i = 0; i < height; i++) {
            for (int j = 0; j < width; j++) {
                int x = (int) (j * squareSize - panOffsetX);
                int y = (int) (i * squareSize - panOffsetY);

                if (grid[i][j]) {
                    g.setColor(WHITE);
                    g.fillRect(x, y, squareSize, squareSize);
                } else {
                    g.setColor(BLACK);
                    g.fillRect(x, y, squareSize, squareSize);
                }
            }
        }
    }

    /**
     * Updates the grid based on the Game of Life rules.
     * Repaints the frame to reflect the changes.
     */
    public void updateGrid() {
        if (gameState) {
            grid = logic.gridUpdate(grid);
            frame.repaint();
        }
    }

    // ... (rest of the code)
}

I know that if we take our newPanOffsetX/Y restriction off,

        // Limit the pan offset to avoid going out of bounds
        newPanOffsetX = Math.max(0, Math.min(newPanOffsetX, maxPanOffsetX));
        newPanOffsetY = Math.max(0, Math.min(newPanOffsetY, maxPanOffsetY));

then it gets in negative numbers very quickly. Each time when we start dragging our mouse in any direction - we make things worse. That is probably the reason why it gets back to the top-left corner.

Would appreciate any help on this problem, cheers!

Upvotes: 1

Views: 40

Answers (1)

Yehor Boiar
Yehor Boiar

Reputation: 11

I managed to fix this bug. By changing my MyMouseListener.

Old version of code

package input;

import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.SwingUtilities;

import renderer.Renderer;

/**
 * Custom MouseAdapter for handling mouse events in the Renderer.
 * This adapter is responsible for processing mouse clicks, releases, and drag events.
 */
public class MyMouseListener extends MouseAdapter {
    private final Renderer renderer;

    /**
     * Constructs a new MyMouseListener with the specified Renderer.
     *
     * @param renderer The Renderer associated with this adapter.
     */
    public MyMouseListener(Renderer renderer) {
        this.renderer = renderer;
    }

    /**
     * Invoked when the mouse is clicked.
     * Calls the handleMouseClick method to update the grid based on the mouse click.
     *
     * @param e The MouseEvent representing the mouse click event.
     */
    @Override
    public void mouseClicked(MouseEvent e) {
        // Check if the left mouse button is pressed (LMB). 
        // for some reason the never version BUTTON1_DOWN_MASK doesn't work, so I used the older version
        if ((e.getModifiers() & InputEvent.BUTTON1_MASK) == InputEvent.BUTTON1_MASK) {
            handleLMBClick(e);
        }
    }

    /**
     * Handles the mouse click event by updating the grid according to the click position.
     *
     * @param e The MouseEvent representing the mouse click event.
     */
    
    public void handleLMBClick(MouseEvent e) {
 
            int squareSize = (int) (10 * renderer.getZoomFactor());
        
            int x = (int) ((e.getX() + renderer.getPanOffsetX()) / squareSize);
            int y = (int) ((e.getY() + renderer.getPanOffsetY() - 40) / squareSize);
        
            // Clamp the indices to valid grid bounds
            x = Math.min(Math.max(0, x), renderer.getWidth() - 1);
            y = Math.min(Math.max(0, y), renderer.getHeight() - 1);
        
            renderer.reverseElement(y, x);
            System.out.println("Mouse Clicked: " + x + "," + y);
            renderer.getFrame().repaint(); // Repaint the frame to update the drawing
        
    }

    /**
     * Invoked when the mouse is released.
     * Resets the last mouse coordinates to -1, indicating no ongoing drag.
     *
     * @param e The MouseEvent representing the mouse release event.
     */
    // @Override
    // public void mouseReleased(MouseEvent e) {
    //     renderer.setLastMouseX(-1);
    //     renderer.setLastMouseY(-1);
    // }
}

New version of code

package input;

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.SwingUtilities;

import renderer.Renderer;

/**
 * Custom MouseAdapter for handling mouse events in the Renderer.
 * This adapter is responsible for processing mouse clicks, releases, and drag
 * events.
 */
public class MyMouseListener extends MouseAdapter {
    private final Renderer renderer;

    /**
     * Constructs a new MyMouseListener with the specified Renderer.
     *
     * @param renderer The Renderer associated with this adapter.
     */
    public MyMouseListener(Renderer renderer) {
        this.renderer = renderer;
    }

    /**
     * Handles the mouse pressed event, updating the grid based on the click
     * position.
     * If the right mouse button is pressed, it records the initial mouse
     * coordinates for panning.
     * If the left mouse button is pressed, it calculates the grid indices and
     * toggles the cell state.
     * @param e The MouseEvent representing the mouse click event.
     */
    @Override
    public void mousePressed(MouseEvent e) {
        if (SwingUtilities.isRightMouseButton(e)) {
            renderer.setLastMouseX(e.getX());
            renderer.setLastMouseY(e.getY());
        } else {
            int squareSize = (int) (10 * renderer.getZoomFactor());

            int x = (int) ((e.getX() + renderer.getPanOffsetX()) / squareSize);
            int y = (int) ((e.getY() + renderer.getPanOffsetY() - 40) / squareSize);

            // Clamp the indices to valid grid bounds
            x = Math.min(Math.max(0, x), renderer.getWidth() - 1);
            y = Math.min(Math.max(0, y), renderer.getHeight() - 1);

            renderer.reverseElement(y, x);
            System.out.println("Mouse Clicked: " + x + "," + y);
            renderer.getFrame().repaint(); // Repaint the frame to update the drawing
        }
    }

}

Upvotes: 0

Related Questions