Действие JButton вызвано событием focusLost. Как это возможно?

Один из наших клиентов сообщил об исключении в нашем приложении. Проблема в том, что я совершенно не могу понять, как этот баг может быть воспроизведен.

Вот код:

btn.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        popup.show(btn, 3, btn.getHeight());
    }
});

Примечания :

  • btn — это последняя локальная переменная типа JButton.
  • popup — это последняя локальная переменная типа JPopupMenu.

Было выброшено следующее исключение:

java.awt.IllegalComponentStateException: component must be showing on the screen to determine its location
    at java.awt.Component.getLocationOnScreen_NoTreeLock(Unknown Source)
    at java.awt.Component.getLocationOnScreen(Unknown Source)
    at javax.swing.JPopupMenu.show(Unknown Source)
    at fr.def.iss.vd2.mod_site_watcher_gui.SiteElementPanel$4.actionPerformed(SiteElementPanel.java:117)
    at javax.swing.AbstractButton.fireActionPerformed(Unknown Source)
    at javax.swing.AbstractButton$Handler.actionPerformed(Unknown Source)
    at javax.swing.DefaultButtonModel.fireActionPerformed(Unknown Source)
    at javax.swing.DefaultButtonModel.setPressed(Unknown Source)
    at javax.swing.plaf.basic.BasicButtonListener.focusLost(Unknown Source)
    at java.awt.Component.processFocusEvent(Unknown Source)
    at java.awt.Component.processEvent(Unknown Source)
    at java.awt.Container.processEvent(Unknown Source)
    at java.awt.Component.dispatchEventImpl(Unknown Source)
    at java.awt.Container.dispatchEventImpl(Unknown Source)
    at java.awt.Component.dispatchEvent(Unknown Source)
    at java.awt.KeyboardFocusManager.redispatchEvent(Unknown Source)
    at java.awt.DefaultKeyboardFocusManager.typeAheadAssertions(Unknown Source)
    at java.awt.DefaultKeyboardFocusManager.dispatchEvent(Unknown Source)
    at java.awt.Component.dispatchEventImpl(Unknown Source)
    at java.awt.Container.dispatchEventImpl(Unknown Source)
    at java.awt.Component.dispatchEvent(Unknown Source)
    at java.awt.EventQueue.dispatchEventImpl(Unknown Source)
    at java.awt.EventQueue.access$000(Unknown Source)
    at java.awt.EventQueue$1.run(Unknown Source)
    at java.awt.EventQueue$1.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.security.AccessControlContext$1.doIntersectionPrivilege(Unknown Source)
    at java.security.AccessControlContext$1.doIntersectionPrivilege(Unknown Source)
    at java.awt.EventQueue$2.run(Unknown Source)
    at java.awt.EventQueue$2.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.security.AccessControlContext$1.doIntersectionPrivilege(Unknown Source)
    at java.awt.EventQueue.dispatchEvent(Unknown Source)
    at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
    at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
    at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
    at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
    at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
    at java.awt.EventDispatchThread.run(Unknown Source)

Насколько я понимаю, метод show жалуется, что btn не показывает. Как возможно, что btn не отображается при вызове его метода actionPerformed?

Самое странное в этой трассировке стека то, что метод actionPerformed, по-видимому, запускается во время обработки FocusEvent (на самом деле focusLost).

Вопрос в следующем: можете ли вы объяснить, как может произойти эта трассировка стека?

Эпилог

Благодаря предложению от trashgod я нашел проблему.

В Windows когда кнопка исчезает при нажатии, срабатывают ее ActionListeners, как если бы кнопка была нажата. Такое поведение можно наблюдать в Windows, но не в Linux.

Я зарегистрировал ошибку в базе данных ошибок Oracle/Sun. Ссылка здесь :

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7115421

(эта ссылка станет действительной в течение нескольких дней после ее проверки командой Java).

Спасибо за вашу помощь. Ответы от trashgod и Томаса очень помогли.


person barjak    schedule 22.11.2011    source источник
comment
проблема в коде, который вы не показываете   -  person kleopatra    schedule 22.11.2011
comment
@kleopatra Возможно, это так. Окружающий код не имеет отношения к вопросу и только внесет в него шум. Я могу только сказать, что 1) JButton создается либо включенным, либо отключенным (после этого он не включается или не отключается) 2) контейнер JButton может быть удален из его иерархии 3) видимость (setVisible) JButton никогда не изменяется 4) actionPerformed никогда не вызывается явно моим кодом 5) экземпляр JButton добавляется в контейнер, но на него нигде больше не ссылаются. Также нет кода, который пытается получить доступ к JButton через его контейнер.   -  person barjak    schedule 22.11.2011
comment
Как многие говорили, требуется SSCCE. Мы знаем, что вы делаете с ActionListener, но мы не знаем, что еще вы делаете со своей кнопкой. Нам нужно увидеть этот код, если мы хотим вам помочь, а фрагмента кода, который вы разместили, недостаточно для того, чтобы мы могли найти проблему. Если проблему нельзя воспроизвести, если просто добавить ту же кнопку в пустой фрейм, то проблема кроется где-то еще в вашем коде.   -  person Laf    schedule 22.11.2011
comment
@Laf проблема вообще не может быть воспроизведена. Так что нет, я не могу дать вам SSCCE. Природа вопроса запрещает это.   -  person barjak    schedule 22.11.2011
comment
Окружающий код не имеет отношения к вопросу, который явно неверен, поскольку показанный фрагмент выглядит нормально и не может вызвать ошибку :-) приложите все усилия к воспроизведению проблемы - это единственный способ найти основную причину. Без всего, что вы получаете здесь, это дикие догадки   -  person kleopatra    schedule 23.11.2011
comment
приложите все усилия, чтобы воспроизвести проблему, это именно то, чего я пытаюсь добиться: воспроизвести проблему, которая возникла только один раз, на машине, к которой я не могу получить доступ. Если я смогу воспроизвести проблему, я выиграю! Если вам действительно нужен полный код класса, вот он: pastebin.com/J2iU2kpd   -  person barjak    schedule 23.11.2011


Ответы (5)


Одним из возможных источников является состояние гонки, которое позволяет активировать событие до того, как получатель станет видимым. Убедитесь, что ваши объекты Swing GUI созданы и управляются только на поток отправки событий. Статья Отладка Swing, окончательный итог цитируется в Как генерировать исключения из RepaintManager упоминает несколько подходов к автоматизации поиска.

person trashgod    schedule 22.11.2011
comment
Спасибо за предложение. Я сделал ручной статический анализ кода и не увидел возможности нарушения EDT. Я также использовал инструмент для поиска нарушений EDT во время выполнения (SwingExplorer) и пытался максимально стимулировать этот компонент. Инструмент сообщил о некоторых нарушениях, но ничего не имело отношения к этому конкретному фрагменту кода. Но я исправил эти нарушения. - person barjak; 23.11.2011
comment
Глядя на getLocationOnScreen(), возможно, одноранговый узел стал null после непреднамеренного dispose()? Может ли всплывающее окно привести к тому, что isShowing() вернет false? - person trashgod; 23.11.2011
comment
То одно, то другое... Вопрос в том, как можно вызвать actionPerformed, если его коллегой является null или если кнопка не отображается? - person barjak; 23.11.2011
comment
Пир не должен исчезать, но он принадлежит хосту. Неуместный dispose() найти может быть сложнее. Выполните любое из предостережений в isShowing() подать заявку? - person trashgod; 24.11.2011
comment
Нет, кнопка не находится в области прокрутки и не может быть скрыта другим компонентом. Но контейнер кнопки может быть удален из ее иерархии. - person barjak; 24.11.2011
comment
Я также видел скрытые проблемы синхронизации без EDT, которые проявлялись только на определенных платформах, например. Mac OS на PowerPC. Можете ли вы протестировать что-то, приближенное к среде клиента? - person trashgod; 24.11.2011
comment
Ты был прав. Я могу сейчас воспроизвести этот баг, но только на винде (разрабатываю на линуксе). Я не думал попробовать на другой платформе, так как мы используем кросс-платформенный внешний вид, и ничто в трассировке стека, похоже, не связано с базовой платформой. Я обновил свой вопрос, чтобы объяснить проблему. - person barjak; 24.11.2011

•btn — конечная локальная переменная типа JButton.

Может быть, это проблема. Возможно, у вас есть ссылка на компонент, который не виден на экране.

Вместо этого вы должны использовать:

JButton button = (JButton)e.getSource();

Тогда вы точно знаете, что ссылаетесь на компонент, сгенерировавший событие.

Кроме того, убедитесь, что у вас нет переменных класса с тем же именем.

person camickr    schedule 22.11.2011
comment
Сам слушатель зарегистрирован на btn. Таким образом, возвращаемое значение getSource() всегда должно быть btn, не так ли? - person barjak; 22.11.2011
comment
Я хочу сказать, что нет необходимости создавать переменную final. Вы можете получить информацию непосредственно из ActionEvent. Это менее подвержено ошибкам и имеет преимущества в других ситуациях, когда вы можете захотеть разделить ActionListener между несколькими кнопками. В любом случае, вы не опубликовали SSCCE, так что мы просто делаем дикие предположения. - person camickr; 22.11.2011
comment
Боюсь, мой вопрос не может содержать SSCCE, потому что вопрос в том, как можно воспроизвести такое поведение. Если бы я мог опубликовать пример, демонстрирующий проблему (как вы предлагаете), я бы тогда не задавал вопрос :) В любом случае, спасибо за ваш вклад. - person barjak; 22.11.2011
comment
@barjak единственный, у кого есть малейший шанс воспроизвести это, это вы, потому что вы единственный, кто видит это в вашем производственном коде. - person kleopatra; 23.11.2011
comment
@kleopatra да, но сейчас мне не хватает идей :) Полученные ответы мне очень помогли, поэтому я не жалею, что задал этот вопрос. - person barjak; 23.11.2011

Глядя на исходный код DefaultButtonModel#setPressed(...), мы видим следующее:

if(!isPressed() && isArmed()) {
        ...
        fireActionPerformed(
            new ActionEvent(this, ActionEvent.ACTION_PERFORMED,
                            getActionCommand(),
                            EventQueue.getMostRecentEventTime(),
                            modifiers));
} 

Как вы можете видеть, ActionEvent срабатывает, когда кнопка была "взведена", т.е. имела фокус, но не была нажата. Это согласуется с событием «FocusLost».

person Thomas    schedule 22.11.2011
comment
Код вызывающего абонента (в BasicButtonListener) — setArmed(false); setPressed(false);. Я до сих пор не понимаю, как условие if может оцениваться как true. - person barjak; 22.11.2011
comment
+1 в ретроспективе. Я думал, что все платформы будут использовать DefaultButtonModel, но, возможно, стоит попробовать кроссплатформенный L&F в Windows. - person trashgod; 24.11.2011

Вместо этого используйте действие, специфичное для режима:

final JButton button = new JButton();
button.addMouseListener(new MouseAdapter(){
    public void mouseClicked(MouseEvent e) {
       popup.show(btn, 3, btn.getHeight());
    }
})
person Stanislav Levental    schedule 22.11.2011
comment
Спасибо за предложение. Это не отвечает на вопрос, но может быть обходным путем для моей конкретной проблемы. - person barjak; 22.11.2011
comment
Это не очень хорошая идея. Вы должны управлять нажатиями кнопок с помощью ActionLlistener, иначе вы пропустите нажатия кнопок, сгенерированные с клавиатуры (нажатие пробела или ввода, когда фокус находится на кнопке). - person Laf; 22.11.2011
comment
@Laf, вы правы, но преимущество в том, что если только щелчок мыши может вызвать popup.show, то мы можем быть уверены, что кнопка действительно отображается. Я согласен, что это не очень хорошо, но это может быть обходным путем. - person barjak; 22.11.2011

Я не могу устоять, я только предпочитаю PopupFactory

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

public class UsePopupFactory {

    private JFrame frame = new JFrame("PopupFactory Sample");
    private PopupFactory factory = PopupFactory.getSharedInstance();
    private Popup popup;

    public UsePopupFactory() {
        JPanel btnPanel = new JPanel();
        btnPanel.setBorder(new EmptyBorder(20, 20, 20, 20));
        btnPanel.setLayout(new GridLayout(0, 3));
        ActionListener actionListener = new ShowPopup(frame);
        JButton start3 = new JButton("Pick Me for Popup");
        JButton start = new JButton("Pick Me for Popup");
        JButton start2 = new JButton("Pick Me for Popup");
        btnPanel.add(start3);
        btnPanel.add(start);
        btnPanel.add(start2);
        start3.setVisible(false);
        start2.setVisible(false);
        start.addActionListener(actionListener);
        Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(btnPanel, BorderLayout.SOUTH);
        frame.setSize(new Dimension(d.width / 4, d.height / 4));
        frame.setVisible(true);
    }

    private class ShowPopup implements ActionListener {

        private Component component;

        ShowPopup(Component component) {
            this.component = component;
        }

        public synchronized void actionPerformed(ActionEvent actionEvent) {
            JPanel pnl = new JPanel();
            JComboBox combo = new JComboBox();
            JButton button = new JButton("any action");
            pnl.add(combo);
            pnl.add(button);
            pnl.setPreferredSize(new Dimension(250, 40));
            popup = factory.getPopup(component, pnl,
                    frame.getWidth() / 2 - pnl.getPreferredSize().width / 2,
                    frame.getHeight() / 2 - pnl.getPreferredSize().height / 2);
            popup.show();
            Timer timer = new Timer(3000, hider);
            timer.start();
        }
    }
    private Action hider = new AbstractAction() {

        private static final long serialVersionUID = 1L;

        @Override
        public void actionPerformed(ActionEvent e) {
            popup.hide();
        }
    };

    public static void main(final String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                UsePopupFactory uPF = new UsePopupFactory();
            }
        });
    }
}
person mKorbel    schedule 22.11.2011
comment
хммм неправдоподобный ответ, я написал это на лету, спасибо за ответ - person mKorbel; 23.11.2011