Search code examples
javaswinguser-interfacedrag-and-dropmouselistener

JLabel not clicking into the right place when mouseReleased() is implemented


so I am working on a Solitaire project and am currently severely stuck on the GUI part. I am trying to do drag and drop using Rob Camick's OverlapLayout (source code here) but it doesn't work out how I want it to.

I can pick up the card and move it just fine, but when I release it, it doesn't drop down where I want it to and the card disappears. How to fix this?

Attached is a simplified version of my code. When that code is run, as seen in the attached image, the card is dragged from one stack to another, but then it automatically moves to the bottom of the screen instead of on top of the card it was dropped on.

This is the class with the problem (I believe that it has to do with the way I created the mainStacks using the for loop, because when I was dragging, the drag layer went behind some of the stacks themselves):

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;

public class Test extends JPanel implements MouseListener, MouseMotionListener {
    
    //instance variables
    private static final int CARD_WIDTH = 73;
    private static final int CARD_HEIGHT = (int)(CARD_WIDTH*1.49137931+.5);
    private static final int MAIN_STACKS_X_DIFF = 35;
    private static final int MAIN_STACKS_Y_DIFF = 30;
    
    private final Dimension cardSize = new Dimension(CARD_WIDTH, CARD_HEIGHT);
    
    private JPanel mainStacks, mainWrapper;
        
    private Component card;
    private int xAdj, yAdj;
    
    private Deck deckOfCards;
        
    //methods
    
    public Test() {
        JFrame gameScreen = new JFrame();
        gameScreen.setTitle("Solitaire");
        gameScreen.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        gameScreen.add(this);
        
        deckOfCards = new Deck();
        deckOfCards.shuffleDeck();
        
        mainStacks = makeMainStacks();
        mainWrapper = new JPanel();
        mainWrapper.setBackground(new Color(0,153,0));
        mainWrapper.add(mainStacks);
        
        gameScreen.setLayout(new BorderLayout());
        gameScreen.add(mainWrapper, BorderLayout.CENTER);
        
        setLayout(null);
        gameScreen.setSize(800,600);
        gameScreen.setResizable(false);
        gameScreen.getContentPane().setBackground(new Color(0,153,0));
        gameScreen.setVisible( true );
    }
        
    private JPanel makeMainStacks() {
        mainStacks = new JPanel(new GridLayout(0,7,MAIN_STACKS_X_DIFF, MAIN_STACKS_Y_DIFF));
        for (int stack = 0; stack < 7; stack++) {
            
            JPanel mainStack = new JPanel( new OverlapLayout( new Point(0, 22)));
            mainStack.setBackground( new Color(0,153,0));
            mainStack.setBounds(10,0,CARD_WIDTH, CARD_HEIGHT);
            mainStack.setBorder( new EmptyBorder(10, 0, CARD_WIDTH, CARD_HEIGHT));
            mainStack.setPreferredSize(new Dimension(CARD_WIDTH, CARD_HEIGHT + 250));
            mainStacks.add(mainStack);
            for (int cardInStack = 0; cardInStack <= stack; cardInStack++) {   
                
                Card placementCard = deckOfCards.drawCard(); //draws card
                placementCard.setIsFaceUp(false);
                //checking if the card is the last card in the stack
                if (stack == cardInStack) {
                    System.out.println(stack + cardInStack);
                    placementCard.setIsFaceUp(true); //card is facing up
                    //sizes the image and sets it to placementCard
                    ImageIcon placementCardFace = new ImageIcon(placementCard.getImagePath());
                    Image placementCardScaled = placementCardFace.getImage().getScaledInstance(CARD_WIDTH, CARD_HEIGHT, Image.SCALE_SMOOTH);
                    JLabel cardLabel = new JLabel(new ImageIcon(placementCardScaled));
                    mainStack.add(cardLabel);
                } //end of if statement
                
                //if statement to check whether the card should be revealed or not
                if (!placementCard.getIsFaceUp()) { //hide cardFront
                    //sizes the image and sets it to placementCard
                    ImageIcon placementCardBack = new ImageIcon(Card.getBackImagePath());
                    Image placementCardScaled = placementCardBack.getImage().getScaledInstance(CARD_WIDTH, CARD_HEIGHT, Image.SCALE_SMOOTH);
                    JLabel cardLabel = new JLabel(new ImageIcon(placementCardScaled));
                    cardLabel.setPreferredSize(cardSize);
                    mainStack.add(cardLabel);
                } //end of if statement
            }
            mainStack.addMouseListener(this);
            mainStack.addMouseMotionListener(this);
        }
        
        JPanel mainStacksWrapper = new JPanel();
        mainStacksWrapper.add(mainStacks);
        mainStacksWrapper.setBackground(new Color(0,153,0));
        mainStacks.setBackground(new Color(0,153,0));
        return mainStacksWrapper;
    }
    
    
    @Override
    public void mousePressed(MouseEvent e) {
        try {
        JPanel stack = (JPanel)e.getComponent();
        card = stack.getComponent(0);
        
        if (card instanceof JPanel){}
        
        Point stackPos = card.getParent().getLocation();
        xAdj = stackPos.x-e.getX();
        yAdj = stackPos.y-e.getY();
        card.setLocation(e.getX() + xAdj + 35, e.getY() + yAdj);
        System.out.println(xAdj + yAdj);
        
        JLayeredPane lp = getRootPane().getLayeredPane();
        lp.add(card,JLayeredPane.DRAG_LAYER);
        setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
        } catch (ArrayIndexOutOfBoundsException err) {
            System.err.println("No card in stack. Cannot execute move.");
        }
    }
    
    @Override
    public void mouseDragged(MouseEvent e) {
        if (card==null){}
        
        int x = e.getX() + xAdj + 35;
        int y = e.getY() + yAdj;
        card.setLocation(x,y);
    }
    
    @Override
    public void mouseReleased(MouseEvent e) {
        setCursor(null);

        if (card == null) return;

        //  Make sure the card is no longer painted on the layered pane

        card.setVisible(false);

        //  check to see if card was dragged to a stack

        JPanel stackPanel = null;
        Point cardLocation = card.getLocation();
        cardLocation.y -= mainStacks.getParent().getLocation().y;

        for (int i = 0; i < mainStacks.getComponentCount(); i++)
        {
            JPanel panel = (JPanel)mainStacks.getComponent(i);

            if (panel.getBounds().contains(cardLocation))
            {
                stackPanel = panel;
                break;
            }
        }

        if (stackPanel == null)
            mainStacks.add( card );
        else
            stackPanel.add( card );

        card.setVisible(true);
    }
    
    @Override
    public void mouseClicked(MouseEvent e) {}
    @Override
    public void mouseMoved(MouseEvent e) {}
    @Override
    public void mouseEntered(MouseEvent e) {}
    @Override
    public void mouseExited(MouseEvent e) {}
    
    public static void main(String[] args) {
        new Test();
    }
}

These next few classes are just additional classes to help the code compile:

import java.util.ArrayList;
import java.util.Collections;

public class Deck {
    
    //instance variables
    private final ArrayList<Card> cards;
    private int drawCardIndex;
    
    //methods
    /**
     * This constructor for the Deck class creates all 52 cards and adds them to cards List.
     */
    public Deck() {
        cards = new ArrayList<>(); //creating new deck
        
        for (Suit suit: Suit.values()) { //loops through suits
            for (Rank rank: Rank.values()) { //loops through ranks
                cards.add(new Card(suit, rank)); //adds a unique Card to cards
            }
        }
        drawCardIndex = 0; //initializes index
    } //end of constructor
    
    /**
     * This method returns the size of the deck.
     * @return int - size of deck
     */
    public int getSize() {
        return cards.size();
    } //end of getSize method
    
    /**
     * This method returns a drawn card in the deck of cards.
     * @return Card - a new drawn card in the deck of cards.
     */
    public Card drawCard() {
        if (drawCardIndex<cards.size()) { //checks if all of the cards have been drawn
            Card card = cards.get(drawCardIndex); //draws a card
            drawCardIndex++; //increments
            return card; //return statement
        }
        return null; //returns null if all of the cards have already been drawn
    } //end of drawCard method
    
    /**
     * This method shuffles the deck of cards.
     */
    public void shuffleDeck() {
        Collections.shuffle(cards); //mixes ArrayList
        resetDrawCardIndex(); //resets the drawCardIndex variable
    } //end of shuffleDeck method
    
    /**
     * This method resets the drawCardIndex.
     */
    public void resetDrawCardIndex() {
        drawCardIndex = 0;
    } //end of resetDrawCardIndex method
    
    /**
     * This method removes a card from the deck.
     * @param card the card to be removed from the deck
     */
    public void removeCardFromDeck(Card card) {
        //checks if the stack is empty
        cards.remove(card); //removes the card from the deck
    } //end of removeCardFromStack method
    
    /**
     * This method checks if all of the cards have been drawn.
     * @return Boolean true or false based on if all the cards have been drawn or not.
     */
    public boolean isEmpty() {
        return cards.size() <= drawCardIndex;
    } //end of isEmpty method
    
} //end of Deck class
public class Card {
    //instance variables
    private final Suit SUIT;
    private final Rank RANK;
    private boolean isFaceUp;
    
    //methods
    /**
     * This is the constructor method for the Card class.
     * @param suit - the suit to be assigned to the card.
     * @param rank - the rank to be assigned to the card.
     */
    public Card(Suit suit, Rank rank) {
        this.SUIT = suit;
        this.RANK = rank;
        this.isFaceUp = false;
    } //end of constructor
    
    /**
     * This method gives the user the suit of the Card.
     * @return Suit - the suit of the Card.
     */
    public Suit getSuit() {
        return this.SUIT;
    } //end of getSuit method
    /**
     * This method gives the user the rank of the Card.
     * @return Rank - the rank of the Card.
     */
    public Rank getRank() {
        return this.RANK;
    } //end of getRank method
    
    /**
     * This method gives whether the Card is faced up or not.
     * @return Boolean - true or false based on if the Card is facing up.
     */
    public boolean getIsFaceUp() {
        return this.isFaceUp;
    } //end of getIsFaceUp method
    /**
     * This method sets the isFaceUp variable to true if the card is facing up in the game.
     */
    public void setIsFaceUp(boolean b) {
        this.isFaceUp = b;
    } //end of setIsFaceUp method
    
    /**
     * This method determines if the Card is from a red suit.
     * @return Boolean - true or false if card is red
     */
    public boolean isRed() {
        return this.SUIT == Suit.HEARTS || this.SUIT == Suit.DIAMONDS;
    } //end of isRed method
    /**
     * This method determines if the Card is from a black suit.
     * @return a Boolean true or false if card is black
     */
    public boolean isBlack() {
        return this.SUIT == Suit.CLUBS || this.SUIT == Suit.SPADES;
    } //end of isBlack method
    
    /**
     * This method determines if the card can be stacked on the card behind it.
     * @return a Boolean true or false if it can be stacked
     */
    public boolean stackable(Card card) {
        return this.isRed() != card.isRed();
    } //end of stackable method
    
    /**
     * This method gives the name of each card.
     * @return a string that has two letters- the first letter of the rank and the first letter of the suit.
     */
    public String getName() {
        String number = ""; //variable declaration
        switch(RANK) { //switch to assign rank name
            case ACE -> number = "A"; //case
            case TWO -> number = "2"; //case
            case THREE -> number = "3"; //case
            case FOUR -> number = "4"; //case
            case FIVE -> number = "5"; //case
            case SIX -> number = "6"; //case
            case SEVEN -> number = "7"; //case
            case EIGHT -> number = "8"; //case
            case NINE -> number = "9"; //case
            case TEN -> number = "10"; //case
            case JACK -> number = "J"; //case
            case QUEEN -> number = "Q"; //case
            case KING -> number = "K"; //case
        } //end of switch
        String suitName = ""; //variable declaration
        switch(SUIT) { //switch to assign suit name
            case HEARTS -> suitName = "H"; //case
            case DIAMONDS -> suitName = "D"; //case
            case SPADES -> suitName = "S"; //case
            case CLUBS -> suitName = "C"; //case
        } //end of switch
        return number + suitName; //return statement
    } //end of getName method
    
    /**
     * This method gives the path to a corresponding image.
     * @return String - returns the path of the image corresponding with the card.
     */
    public String getImagePath() {
        return "C:\\ICS3U_SUMMATIVE_SOLITAIRE\\src\\cards\\" + getName().substring(1,getName().length()) + "\\" + getName() + ".jpg"; //return statement
    } //end of getImage method
    /**
     * This method gives the file path to the back of the card image.
     * @return String the file path to the card back image
     */
    public static String getBackImagePath() {
        return "C:\\ICS3U_SUMMATIVE_SOLITAIRE\\src\\CardBack.jpg";
    } //end of getImageBackPath method
    
    @Override
    /**
     * This method returns the description about the instance variables.
     * @return String - rank and suit of card along with image path.
     */
    public String toString() {
        return this.RANK + " of " + this.SUIT + " ---- " + getImagePath();
    }
} //end of Card class
public enum Suit {
   //enum fields
   /**
    * Hearts, Diamonds,
    */
   HEARTS,DIAMONDS,
   /**
    * Spades, Clubs
    */
   SPADES,CLUBS;
}
public enum Rank {
   //enum fields
   /**
    * 2,3,4,5,6,7,8,9,10
    */
   TWO,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,NINE,TEN,
   /**
    * A,J,Q,K
    */
   ACE,JACK,QUEEN,KING;
}

I have reviewed past suggestions from SO, but those do not seem to help my problem. Also have tried altering the y values to see if that helps, but it didn't.


Solution

  • First...

    A mouseEvent reports it's location relative to the component which generated the event, that is, the top/left position (0x0) is relative to the component.

    Since you are moving components between coordinate contexts, you need to convert the event point accordingly, luckily, there is a simple way to do this - SwingUtilities.convertMouseEvent(Component, Point, Component)

        @Override
        public void mousePressed(MouseEvent e) {
            try {
                JPanel stack = (JPanel) e.getComponent();
                card = stack.getComponent(0);
    
                if (card instanceof JPanel) {
                }
                
                Point cardInWorld = SwingUtilities.convertPoint(card.getParent(), card.getLocation(), getRootPane().getLayeredPane());
                Point pointInWorld = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), getRootPane().getLayeredPane());
                
                Point stackPos = card.getParent().getLocation();
                xAdj = pointInWorld.x - cardInWorld.getLocation().x;
                yAdj = pointInWorld.y - cardInWorld.getLocation().y;
    
                JLayeredPane lp = getRootPane().getLayeredPane();
                lp.add(card, JLayeredPane.DRAG_LAYER);
                card.setLocation(cardInWorld);
                setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
            } catch (ArrayIndexOutOfBoundsException err) {
                System.err.println("No card in stack. Cannot execute move.");
            }
        }
    
        @Override
        public void mouseDragged(MouseEvent e) {
            if (card == null) {
            }
            
            Point pointInWorld = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), getRootPane().getLayeredPane());
    
            int x = pointInWorld.x - xAdj;
            int y = pointInWorld.y - yAdj;
            card.setLocation(x, y);
        }
    

    This will cause the card to maintained relative to the mouse cursor - so it's position (relative to the mouse cursor) won't change. This makes it so that the mouse cursor's point is the source of the drop, not the card's position.

    Second...

    Walking a container's contents like this...

    @Override
    public void mouseReleased(MouseEvent e) {
        setCursor(null);
    
        if (card == null) return;
    
        //  Make sure the card is no longer painted on the layered pane
    
        card.setVisible(false);
    
        //  check to see if card was dragged to a stack
    
        JPanel stackPanel = null;
        Point cardLocation = card.getLocation();
        cardLocation.y -= mainStacks.getParent().getLocation().y;
    
        for (int i = 0; i < mainStacks.getComponentCount(); i++)
        {
            JPanel panel = (JPanel)mainStacks.getComponent(i);
    
            if (panel.getBounds().contains(cardLocation))
            {
                stackPanel = panel;
                break;
            }
        }
    
        if (stackPanel == null)
            mainStacks.add( card );
        else
            stackPanel.add( card );
    
        card.setVisible(true);
    }
    

    is always a bad idea and took me some significant time to suss out.

    The source of the problem starts right here...

    private JPanel makeMainStacks() throws IOException {
        mainStacks = new JPanel(new GridLayout(0, 7, MAIN_STACKS_X_DIFF, MAIN_STACKS_Y_DIFF));
        //...
        JPanel mainStacksWrapper = new JPanel();
        mainStacksWrapper.add(mainStacks);
        mainStacksWrapper.setBackground(new Color(0, 153, 0));
        mainStacks.setBackground(new Color(0, 153, 0));
        return mainStacksWrapper;
    }
    

    You add mainStacks into a "wrapper" container, and then assign it to mainStacks (I'm so confused).

    The problem with this is, when you walk the container's contents, it only contains a single child. The original mainStacks, which actually contains all the child stacks you actually want.

    A better solution would be to add each stack component to a List and simply iterate over it instead, that way, you're not going to get caught out when the UI structure changes.

    Start by creating a new instance field...

    private List<JPanel> stacks = new ArrayList<>(7);
    

    Then in your makeMainStacks add each stack to it...

    private JPanel makeMainStacks() throws IOException {
        JPanel mainStacks = new JPanel(new GridLayout(0, 7, MAIN_STACKS_X_DIFF, MAIN_STACKS_Y_DIFF));
        mainStacks.setBackground(Color.MAGENTA);
        for (int stack = 0; stack < 7; stack++) {
    
            JPanel mainStack = new JPanel(new OverlapLayout(new Point(0, 22)));
            mainStack.setBounds(10, 0, CARD_WIDTH, CARD_HEIGHT);
            mainStack.setBorder(
                    new CompoundBorder(new LineBorder(Color.RED), new EmptyBorder(10, 0, CARD_WIDTH, CARD_HEIGHT))
            );
            mainStack.setPreferredSize(new Dimension(CARD_WIDTH, CARD_HEIGHT + 250));
            mainStacks.add(mainStack);
    
            stacks.add(mainStack);
    
            for (int cardInStack = 0; cardInStack <= stack; cardInStack++) {
    
                Card placementCard = deckOfCards.drawCard(); //draws card
                placementCard.setIsFaceUp(false);
                //checking if the card is the last card in the stack
                if (stack == cardInStack) {
                    System.out.println(stack + cardInStack);
                    placementCard.setIsFaceUp(true); //card is facing up
                    //sizes the image and sets it to placementCard
                    ImageIcon placementCardFace = new ImageIcon(placementCard.getImagePath());
                    Image placementCardScaled = placementCardFace.getImage().getScaledInstance(CARD_WIDTH, CARD_HEIGHT, Image.SCALE_SMOOTH);
                    JLabel cardLabel = new JLabel(new ImageIcon(placementCardScaled));
                    mainStack.add(cardLabel);
                } //end of if statement
    
                //if statement to check whether the card should be revealed or not
                if (!placementCard.getIsFaceUp()) { //hide cardFront
                    //sizes the image and sets it to placementCard
                    ImageIcon placementCardBack = new ImageIcon(Card.getBackImagePath());
                    Image placementCardScaled = placementCardBack.getImage().getScaledInstance(CARD_WIDTH, CARD_HEIGHT, Image.SCALE_SMOOTH);
                    JLabel cardLabel = new JLabel(new ImageIcon(placementCardScaled));
                    cardLabel.setPreferredSize(cardSize);
                    mainStack.add(cardLabel);
                } //end of if statement
            }
            mainStack.addMouseListener(this);
            mainStack.addMouseMotionListener(this);
        }
    
        JPanel mainStacksWrapper = new JPanel();
        mainStacksWrapper.add(mainStacks);
        mainStacksWrapper.setBackground(new Color(0, 153, 0));
        mainStacks.setBackground(new Color(0, 153, 0));
        return mainStacksWrapper;
    }
    

    And then finally, in mouseReleased, convert the MouseEvent's point to each stack's parents coordinate context and see if you have a hit or not ...

    @Override
    public void mouseReleased(MouseEvent e) {
        setCursor(null);
    
        if (card == null) {
            return;
        }
    
        //  Make sure the card is no longer painted on the layered pane
        card.setVisible(false);
    
        JPanel stackPanel = null;
        for (JPanel stack : stacks) {
            // Check the bounds of each stack against it's parents
            // coordinates context
            Point localPoint = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), stack.getParent());
            if (stack.getBounds().contains(localPoint)) {
                System.out.println("-->");
                stackPanel = stack;
                break;
            }
        }
    
        if (stackPanel == null) {
            mainStacks.add(card);
        } else {
            stackPanel.add(card);
            stackPanel.revalidate();
            stackPanel.repaint();
        }
    
        card.setVisible(true);
    }
    

    Parting thoughts...

    For what it's worth, I wouldn't have done it this way, for all the reasons you're discovering. I would have followed a custom painting route, but I doubt you have time for that now.