Here comes the BasicComboPopup part of my JComboBox work. It's quite a lot. Mostly it fixes how listeners are plugged together and how colors and fonts are installed.
2006-03-17 Roman Kennke <[EMAIL PROTECTED]> * javax/swing/plaf/basic/BasicComboPopup.java (BasicComboPopup): Create listeners here. Configure components here. (show): Correctly calculate bounds using computePopupBounds(). Make scroller fixed-size. Removed special autocloser handling. (hide): Rewritten to use MenuSelectionHandler. (createList): Don't set selection mode here. (configureList): Correctly install colors and fonts and selectionMode. (createScroller): Set scrollpane policies. (configureScroller): Make scroller and scrollbar not-focusable. (configurePopup): Make popup opaque and borderPainted. (installComboBoxListeners): Don't install mouse listener on ComboBox. (delegateFocus): Implemented. (convertMouseEvent): Implemented. (updateListBoxSelectionForEvent): Implemented to also handle autoscrolling. (InvocationMouseHandler.mousePressed): Delegate focus correctly. Only open popup on left mouse-click. (InvocationMouseHandler.mouseReleased): Rewritten. (InvocationMouseMotionHandler.mouseDragged): Rewritten to better support autoscrolling. (ItemHandler.itemStateChanged): Implemented to sync selection with the comboBox. (ListMouseHandler.mouseReleased): Fetch selected index directly from list. (ListMouseMotionHandler.mouseMoved): Only update when mouse is inside the list box. (PropertyChangeHandler.propertyChange): Don't revalidate/repaint here. When model changes, then update listeners correctly. (uninstallListeners): Don't uninstall list listeners. (uninstallComboBoxListeners): Don't uninstall mouse listeners from comboBox. (syncSelection): New helper method. /Roman -- “Improvement makes straight roads, but the crooked roads, without Improvement, are roads of Genius.” - William Blake
Index: javax/swing/plaf/basic/BasicComboPopup.java =================================================================== RCS file: /cvsroot/classpath/classpath/javax/swing/plaf/basic/BasicComboPopup.java,v retrieving revision 1.14 diff -u -r1.14 BasicComboPopup.java --- javax/swing/plaf/basic/BasicComboPopup.java 24 Feb 2006 16:43:33 -0000 1.14 +++ javax/swing/plaf/basic/BasicComboPopup.java 17 Mar 2006 15:35:16 -0000 @@ -59,13 +59,13 @@ import javax.swing.BorderFactory; import javax.swing.ComboBoxModel; import javax.swing.JComboBox; -import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPopupMenu; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.ListCellRenderer; import javax.swing.ListSelectionModel; +import javax.swing.MenuSelectionManager; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.Timer; @@ -165,9 +165,17 @@ public BasicComboPopup(JComboBox comboBox) { this.comboBox = comboBox; - installComboBoxListeners(); + mouseListener = createMouseListener(); + mouseMotionListener = createMouseMotionListener(); + keyListener = createKeyListener(); + + list = createList(); + configureList(); + scroller = createScroller(); + configureScroller(); configurePopup(); - setLightWeightPopupEnabled(comboBox.isLightWeightPopupEnabled()); + installComboBoxListeners(); + installKeyboardActions(); } /** @@ -175,50 +183,40 @@ */ public void show() { - Rectangle cbBounds = comboBox.getBounds(); - - // popup should have same width as the comboBox and should be hight anough - // to display number of rows equal to 'maximumRowCount' property - int popupHeight = getPopupHeightForRowCount(comboBox.getMaximumRowCount()); - - scroller.setPreferredSize(new Dimension(cbBounds.width, popupHeight)); - pack(); - - // Highlight selected item in the combo box's drop down list - if (comboBox.getSelectedIndex() != -1) - list.setSelectedIndex(comboBox.getSelectedIndex()); - - //scroll scrollbar s.t. selected item is visible - JScrollBar scrollbar = scroller.getVerticalScrollBar(); - int selectedIndex = comboBox.getSelectedIndex(); - if (selectedIndex > comboBox.getMaximumRowCount()) - scrollbar.setValue(getPopupHeightForRowCount(selectedIndex)); - - // We put the autoclose-registration inside an InvocationEvent, so that - // the same event that triggered this show() call won't hide the popup - // immediately. - SwingUtilities.invokeLater - (new Runnable() - { - public void run() - { - // Register this popup to be autoclosed when user clicks outside the - // popup. - BasicLookAndFeel laf = (BasicLookAndFeel) UIManager.getLookAndFeel(); - laf.registerForAutoClose(BasicComboPopup.this); - }}); + Dimension size = comboBox.getSize(); + size.height = getPopupHeightForRowCount(comboBox.getMaximumRowCount()); + Rectangle bounds = computePopupBounds(0, comboBox.getBounds().height, + size.width, size.height); + + scroller.setMaximumSize(bounds.getSize()); + scroller.setPreferredSize(bounds.getSize()); + scroller.setMinimumSize(bounds.getSize()); + list.invalidate(); - // location specified is relative to comboBox - super.show(comboBox, 0, cbBounds.height); + syncListSelection(); - } + list.ensureIndexIsVisible(list.getSelectedIndex()); + setLightWeightPopupEnabled(comboBox.isLightWeightPopupEnabled()); + show(comboBox, bounds.x, bounds.y); } /** * This method hides drop down list of items */ public void hide() { - super.setVisible(false); + MenuSelectionManager menuSelectionManager = + MenuSelectionManager.defaultManager(); + javax.swing.MenuElement[] menuElements = + menuSelectionManager.getSelectedPath(); + for (int i = 0; i < menuElements.length; i++) + { + if (menuElements[i] == this) + { + menuSelectionManager.clearSelectedPath(); + break; + } + } + comboBox.repaint(); } /** @@ -270,7 +268,6 @@ public void uninstallingUI() { uninstallComboBoxModelListeners(comboBox.getModel()); - uninstallListeners(); uninstallKeyboardActions(); } @@ -446,7 +443,6 @@ protected JList createList() { JList l = new JList(comboBox.getModel()); - l.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); return l; } @@ -456,9 +452,18 @@ */ protected void configureList() { - list.setModel(comboBox.getModel()); - list.setVisibleRowCount(comboBox.getMaximumRowCount()); + list.setFont(comboBox.getFont()); + list.setForeground(comboBox.getForeground()); + list.setBackground(comboBox.getBackground()); + Color sfg = UIManager.getColor("ComboBox.selectionForeground"); + list.setSelectionForeground(sfg); + Color sbg = UIManager.getColor("ComboBox.selectionBackground"); + list.setSelectionBackground(sbg); + list.setBorder(null); + list.setCellRenderer(comboBox.getRenderer()); list.setFocusable(false); + syncListSelection(); + list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); installListListeners(); } @@ -489,7 +494,8 @@ */ protected JScrollPane createScroller() { - return new JScrollPane(); + return new JScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); } /** @@ -498,8 +504,8 @@ protected void configureScroller() { scroller.setBorder(null); - scroller.getViewport().setView(list); - scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + scroller.setFocusable(false); + scroller.getVerticalScrollBar().setFocusable(false); } /** @@ -508,18 +514,11 @@ */ protected void configurePopup() { + setBorderPainted(true); setBorder(BorderFactory.createLineBorder(Color.BLACK)); - // initialize list that will be used to display combo box's items - this.list = createList(); - ((JLabel) list.getCellRenderer()).setHorizontalAlignment(SwingConstants.LEFT); - configureList(); - - // initialize scroller. Add list to the scroller. - scroller = createScroller(); - configureScroller(); - - // add scroller with list inside of it to JPopupMenu - super.add(scroller); + setOpaque(false); + add(scroller); + setFocusable(false); } /* @@ -528,20 +527,14 @@ */ protected void installComboBoxListeners() { - // mouse listener that listens to mouse event in combo box - mouseListener = createMouseListener(); - comboBox.addMouseListener(mouseListener); - - // mouse listener that listens to mouse dragging events in the combo box - mouseMotionListener = createMouseMotionListener(); - comboBox.addMouseMotionListener(mouseMotionListener); - // item listener listenening to selection events in the combo box itemListener = createItemListener(); comboBox.addItemListener(itemListener); propertyChangeListener = createPropertyChangeListener(); comboBox.addPropertyChangeListener(propertyChangeListener); + + installComboBoxModelListeners(comboBox.getModel()); } /** @@ -651,7 +644,10 @@ */ protected void delegateFocus(MouseEvent e) { - // FIXME: Need to implement + if (comboBox.isEditable()) + comboBox.getEditor().getEditorComponent().requestFocus(); + else + comboBox.requestFocus(); } /** @@ -660,7 +656,7 @@ */ protected void togglePopup() { - if (BasicComboPopup.this.isVisible()) + if (isVisible()) hide(); else show(); @@ -675,7 +671,14 @@ */ protected MouseEvent convertMouseEvent(MouseEvent e) { - return null; + Point point = SwingUtilities.convertPoint((Component) e.getSource(), + e.getPoint(), list); + MouseEvent newEvent= new MouseEvent((Component) e.getSource(), + e.getID(), e.getWhen(), + e.getModifiers(), point.x, point.y, + e.getModifiers(), + e.isPopupTrigger()); + return newEvent; } /** @@ -735,11 +738,24 @@ protected void updateListBoxSelectionForEvent(MouseEvent anEvent, boolean shouldScroll) { - // TODO: We need to handle the shouldScroll parameter somehow. - int index = list.locationToIndex(anEvent.getPoint()); - // Check for valid index. - if (index >= 0) - list.setSelectedIndex(index); + Point point = anEvent.getPoint(); + if (list != null) + { + int index = list.locationToIndex(point); + if (index == -1) + { + if (point.y < 0) + index = 0; + else + index = comboBox.getModel().getSize() - 1; + } + if (list.getSelectedIndex() != index) + { + list.setSelectedIndex(index); + if (shouldScroll) + list.ensureIndexIsVisible(index); + } + } } /** @@ -769,8 +785,11 @@ */ public void mousePressed(MouseEvent e) { - if (comboBox.isEnabled()) - togglePopup(); + if (SwingUtilities.isLeftMouseButton(e) && comboBox.isEnabled()) + { + delegateFocus(e); + togglePopup(); + } } /** @@ -782,28 +801,27 @@ */ public void mouseReleased(MouseEvent e) { - // Get component over which mouse was released - Component src = (Component) e.getSource(); - int x = e.getX(); - int y = e.getY(); - Component releasedComponent = SwingUtilities.getDeepestComponentAt(src, - x, y); - - // if mouse was released inside the bounds of combo box then do nothing, + Component component = (Component) e.getSource(); + Dimension size = component.getSize(); + Rectangle bounds = new Rectangle(0, 0, size.width - 1, size.height - 1); + // If mouse was released inside the bounds of combo box then do nothing, // Otherwise if mouse was released inside the list of combo box items // then change selection and close popup - if (! (releasedComponent instanceof JComboBox)) + if (! bounds.contains(e.getPoint())) { - // List model contains the item over which mouse is released, - // since it is updated every time the mouse is moved over a different - // item in the list. Now that the mouse is released we need to - // update model of the combo box as well. - comboBox.setSelectedIndex(list.getSelectedIndex()); - - if (isAutoScrolling) - stopAutoScrolling(); + MouseEvent convEvent = convertMouseEvent(e); + Point point = convEvent.getPoint(); + Rectangle visRect = new Rectangle(); + list.computeVisibleRect(visRect); + if (visRect.contains(point)) + { + updateListBoxSelectionForEvent(convEvent, false); + comboBox.setSelectedIndex(list.getSelectedIndex()); + } hide(); } + hasEntered = false; + stopAutoScrolling(); } } @@ -827,58 +845,42 @@ */ public void mouseDragged(MouseEvent e) { - // convert point of the drag event relative to combo box list component - // figure out over which list cell the mouse is currently being dragged - // and highlight the cell. The list model is changed but the change has - // no effect on combo box's data model. The list model is changed so - // that the appropriate item would be highlighted in the combo box's - // list. - if (BasicComboPopup.this.isVisible()) + if (isVisible()) { - int cbHeight = (int) comboBox.getPreferredSize().getHeight(); - int popupHeight = BasicComboPopup.this.getSize().height; - - // if mouse is dragged inside the the combo box's items list. - if (e.getY() > cbHeight && ! (e.getY() - cbHeight >= popupHeight)) - { - int index = list.locationToIndex(new Point(e.getX(), - (int) (e.getY() - - cbHeight))); - - int firstVisibleIndex = list.getFirstVisibleIndex(); - - // list.locationToIndex returns item's index that would - // be located at the specified point if the first item that - // is visible is item 0. However in the JComboBox it is not - // necessarily the case since list is contained in the - // JScrollPane so we need to adjust the index returned. - if (firstVisibleIndex != 0) - // FIXME: adjusted index here is off by one. I am adding one - // here to compensate for that. This should be - // index += firstVisibleIndex. Remove +1 once the bug is fixed. - index += firstVisibleIndex + 1; - - list.setSelectedIndex(index); - } - else - { - // if mouse is being dragged at the bottom of combo box's list - // of items or at the very top then scroll the list in the - // desired direction. - boolean movingUP = e.getY() < cbHeight; - boolean movingDown = e.getY() > cbHeight; - - if (movingUP) - { - scrollDirection = SCROLL_UP; - startAutoScrolling(SCROLL_UP); - } - else if (movingDown) - { - scrollDirection = SCROLL_DOWN; - startAutoScrolling(SCROLL_DOWN); - } - } + MouseEvent convEvent = convertMouseEvent(e); + Rectangle visRect = new Rectangle(); + list.computeVisibleRect(visRect); + if (convEvent.getPoint().y >= visRect.y + && (convEvent.getPoint().y <= visRect.y + visRect.height - 1)) + { + hasEntered = true; + if (isAutoScrolling) + stopAutoScrolling(); + Point point = convEvent.getPoint(); + if (visRect.contains(point)) + { + valueIsAdjusting = true; + updateListBoxSelectionForEvent(convEvent, false); + valueIsAdjusting = false; + } + } + else if (hasEntered) + { + int dir = convEvent.getPoint().y < visRect.y ? SCROLL_UP + : SCROLL_DOWN; + if (isAutoScrolling && scrollDirection != dir) + { + stopAutoScrolling(); + startAutoScrolling(dir); + } + else if (!isAutoScrolling) + startAutoScrolling(dir); + } + else if (e.getPoint().y < 0) + { + hasEntered = true; + startAutoScrolling(SCROLL_UP); + } } } } @@ -905,7 +907,13 @@ */ public void itemStateChanged(ItemEvent e) { - // TODO: What should be done here? + if (e.getStateChange() == ItemEvent.SELECTED && ! valueIsAdjusting) + { + valueIsAdjusting = true; + syncListSelection(); + valueIsAdjusting = false; + list.ensureIndexIsVisible(comboBox.getSelectedIndex()); + } } } @@ -924,15 +932,12 @@ public void mousePressed(MouseEvent e) { - // TODO: What should be do here? + // Nothing to do here. } public void mouseReleased(MouseEvent anEvent) { - int index = list.locationToIndex(anEvent.getPoint()); - // Check for valid index. - if (index >= 0) - comboBox.setSelectedIndex(index); + comboBox.setSelectedIndex(list.getSelectedIndex()); hide(); } } @@ -951,7 +956,15 @@ public void mouseMoved(MouseEvent anEvent) { - updateListBoxSelectionForEvent(anEvent, false); + Point point = anEvent.getPoint(); + Rectangle visRect = new Rectangle(); + list.computeVisibleRect(visRect); + if (visRect.contains(point)) + { + valueIsAdjusting = true; + updateListBoxSelectionForEvent(anEvent, false); + valueIsAdjusting = false; + } } } @@ -971,15 +984,21 @@ { if (e.getPropertyName().equals("renderer")) { - list.setCellRenderer((ListCellRenderer) e.getNewValue()); - revalidate(); - repaint(); + list.setCellRenderer(comboBox.getRenderer()); + if (isVisible()) + hide(); } - if (e.getPropertyName().equals("dataModel")) + if (e.getPropertyName().equals("model")) { - list.setModel((ComboBoxModel) e.getNewValue()); - revalidate(); - repaint(); + ComboBoxModel oldModel = (ComboBoxModel) e.getOldValue(); + uninstallComboBoxModelListeners(oldModel); + ComboBoxModel newModel = (ComboBoxModel) e.getNewValue(); + list.setModel(newModel); + installComboBoxModelListeners(newModel); + if (comboBox.getItemCount() > 0) + comboBox.setSelectedIndex(0); + if (isVisible()) + hide(); } } } @@ -991,7 +1010,6 @@ */ private void uninstallListeners() { - uninstallListListeners(); uninstallComboBoxListeners(); uninstallComboBoxModelListeners(comboBox.getModel()); } @@ -1015,12 +1033,6 @@ */ private void uninstallComboBoxListeners() { - comboBox.removeMouseListener(mouseListener); - mouseListener = null; - - comboBox.removeMouseMotionListener(mouseMotionListener); - mouseMotionListener = null; - comboBox.removeItemListener(itemListener); itemListener = null; @@ -1028,6 +1040,15 @@ propertyChangeListener = null; } + void syncListSelection() + { + int index = comboBox.getSelectedIndex(); + if (index == -1) + list.clearSelection(); + else + list.setSelectedIndex(index); + } + // -------------------------------------------------------------------- // The following classes are here only for backwards API compatibility // They aren't used.