What I want seems relatively simple, and I'm almost there. This will eventually be used to extend TableCellEditor
, so the size is important. What I want is something like this:
With a combination of custom ComboBoxEditor
s and ListCellRenderer
s I've been able to get something like this:
Which has the inconveniences of:
JComboBox
JComboBox
to be the height of the JPanel
in the drop-down I'd like to have the drop-down stay visible until the user clicks the editor or the JComboBox
looses focus to another control, and then have the value in the editor update. There will only ever be one (1) JPanel in the drop-down and I don't want the editor to be able to actually edit the string displayed.
My question is similar to @ErkanHaspalut 's question here but neither response is satisfying. I'd previously made a similar attempt by embedding a JPanel
in a JPopupMenu
and adding it to a JTextField
but had similar issues about the popup disappearing prematurely.
I've tried forcing the size of the JComboBox
both by setting the setMaximumSize
height
value (which has no effect) and with
Rectangle tmp = cboTest.getBounds();
tmp.height = 24;
cboTest.setBounds(tmp);
which simply shows the top 24 lines of the JComboBox
. A minimum compilable example would be
/*
* Program to test having a JPanel in the JComboBox's drop-down but not the JComboBox's editor.
*/
package testdropdownsubform;
import java.awt.Component;
import java.awt.Rectangle;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import javafx.application.Application;
import javafx.stage.Stage;
import javax.swing.ComboBoxEditor;
import javax.swing.JComboBox;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.JTextField;
import javax.swing.plaf.basic.BasicComboBoxEditor;
/**
* @author Masked Coder
*/
public class Dim {
public Long DimWidth;
public Long DimHeight;
public Dim () {
DimWidth = 1L;
DimHeight = 1L;
}
@Override
public String toString() {
return DimWidth.toString() + "\' x " + DimHeight.toString() + "\'";
}
}
public class DimPanel extends javax.swing.JPanel {
public DimPanel() {
spnDimWidth = new javax.swing.JSpinner();
spnDimHeight = new javax.swing.JSpinner();
spnDimWidth.setModel(new javax.swing.SpinnerNumberModel(Long.valueOf(1L), Long.valueOf(0L), null, Long.valueOf(1L)));
spnDimWidth.setPreferredSize(new java.awt.Dimension(50, 24));
addComponent(spnDimWidth);
lblTween.setText(" x ");
addComponent(lblTween);
spnDimHeight.setModel(new javax.swing.SpinnerNumberModel(Long.valueOf(1L), Long.valueOf(0L), null, Long.valueOf(1L)));
spnDimHeight.setPreferredSize(new java.awt.Dimension(50, 24));
addComponent(spnDimHeight);
}
private javax.swing.JSpinner spnDimWidth;
private javax.swing.JLabel lblTween;
private javax.swing.JSpinner spnDimHeight;
}
public class DimListCellRenderer implements ListCellRenderer {
private DimPanel dpDim;
public DimListCellRenderer(DimPanel newDimPanel) {
dpDim = newDimPanel;
}
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
Dim dValue = (Dim) value;
dpDim.setDim(dValue);
return dpDim;
}
}
public class DimComboBoxEditor extends BasicComboBoxEditor implements ComboBoxEditor {
JTextField txtDim = new JTextField();
Dim Item = new Dim();
public DimComboBoxEditor() {
txtDim.setEnabled(false);
txtDim.setOpaque(true);
}
@Override
public Component getEditorComponent() {
txtDim.setText(Item.toString());
return txtDim;
}
@Override
public void setItem(Object anObject) {
Item = (Dim) anObject;
}
@Override
public Object getItem() {
return Item;
}
@Override
public void selectAll() {
txtDim.selectAll();
}
@Override
public void addActionListener(ActionListener l) {
txtDim.addActionListener(l);
}
@Override
public void removeActionListener(ActionListener l) {
txtDim.removeActionListener(l);
}
}
public class MainTestForm extends javax.swing.JFrame {
public MainTestForm() {
lblPrevComponent = new javax.swing.JLabel();
chkPrevComponent = new javax.swing.JCheckBox();
lblTest = new javax.swing.JLabel();
cboTest = new JComboBox<testdropdownsubform.DicePanel>();
lblNextComponent = new javax.swing.JLabel();
scpNextComponent = new javax.swing.JScrollPane();
txaNextComponent = new javax.swing.JTextArea();
btnForceHeight = new javax.swing.JButton();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
lblPrevComponent.setText("Prev. Component");
chkPrevComponent.setText("jCheckBox1");
lblTest.setText("Dimension");
cboTest.setEditable(true);
cboTest.setEditor(new DimComboBoxEditor());
cboTest.setRenderer(new DimListCellRenderer(new DimPanel()));
cboTest.addItem(new Dim());
lblNextComponent.setText("Next Component");
txaNextComponent.setColumns(20);
txaNextComponent.setRows(5);
scpNextComponent.setViewportView(txaNextComponent);
btnForceHeight.setText("Force");
btnForceHeight.setToolTipText("Force test combobox height");
btnForceHeight.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
btnForceHeightActionPerformed(evt);
}
});
.addComponent(lblPrevComponent)
.addComponent(chkPrevComponent))
.addComponent(lblTest)
.addComponent(cboTest)
.addComponent(lblNextComponent)
.addComponent(scpNextComponent)
.addComponent(btnForceHeight))
}
private void btnForceHeightActionPerformed(java.awt.event.ActionEvent evt) {
Rectangle tmp = cboTest.getBounds();
tmp.height = 24;
cboTest.setBounds(tmp);
}
/**
* @param args the command line arguments
*/
public static void main(String args[]) {
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new MainTestForm().setVisible(true);
}
});
}
private javax.swing.JButton btnForceHeight;
private javax.swing.JComboBox cboTest;
private javax.swing.JCheckBox chkPrevComponent;
private javax.swing.JLabel lblNextComponent;
private javax.swing.JLabel lblPrevComponent;
private javax.swing.JLabel lblTest;
private javax.swing.JScrollPane scpNextComponent;
private javax.swing.JTextArea txaNextComponent;
}
public class TestDropdownSubform extends Application {
@Override
public void start(Stage primaryStage) {
MainTestForm mtfMain = new MainTestForm();
mtfMain.setVisible(true);
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
My question is, am I missing a detail, or can anyone see a better way to accomplish this? Thanks in advance for your advice.
Edit : Declaring the JComboBox
as
JComboBox cboTest = new JComboBox<DimPanel>() {
private boolean layingOut = false;
@Override
public void doLayout(){
try{
layingOut = true;
super.doLayout();
}finally{
layingOut = false;
}
}
@Override
public Dimension getSize(){
Dimension dim = super.getSize();
if(!layingOut) {
dim.width = Math.max(dim.width, dpThis.getPreferredSize().width);
}
return dim;
}
};
fixes the width of the drop-down. Declaring it as
JComboBox cboTest = new JComboBox<testdropdownsubform.DicePanel>() {
private boolean layingOut = false;
@Override
public void doLayout(){
try{
layingOut = true;
super.doLayout();
}finally{
layingOut = false;
}
}
@Override
public Dimension getSize(){
Dimension dim = super.getSize();
if(!layingOut) {
Dimension dim2 = dpThis.getPreferredSize();
dim.width = Math.max(dim.width, dim2.width);
// dim.height = dim2.height;
}
return dim;
}
@Override
public DimPanel getPrototypeDisplayValue() {
DimPanel tmpPanel = new DimPanel();
if(isCalledFromComboPopup()) {
//
}
else {
Dimension r = dcbeEditor.getPreferredSize();
tmpPanel.setPreferredSize(r);
}
return tmpPanel;
}
/**
* Hack method to determine if called from within the combo popup UI.
*/
public boolean isCalledFromComboPopup() {
try {
final Throwable t = new Throwable();
t.fillInStackTrace();
StackTraceElement[] st = t.getStackTrace();
// look only at top 5 elements of call stack
int max = Math.min(st.length, 5);
for (int i=0; i<max; ++i) {
final String name = st[i].getClassName();
System.out.println(i + ") " + name);
return ((name != null) && name.contains("ComboPopup"));
}
} catch (final Exception e) {
// if there was a problem, assume not called from combo popup
}
return false;
}
};
fixes the editor size but now the drop-down is the width and height of the editor.
So, I ended up going on a different track, creating my own pseudo-combobox with a JTextField
and a JButton
in a JPanel
. I believe I've quite handily genericized the result into four related classes. First, you need an implementation of AbstractPopupPanel
(which quite handily loads into the NetBeans designer if you're using it):
package complexcombobox;
/**
*
* @author MaskedCoder
*
* @param E - the object to be edited in the panel
*/
public abstract class AbstractPopupPanel<E> extends javax.swing.JPanel {
/**
* Creates new form PopupPanel
*/
public AbstractPopupPanel() {
initComponents();
}
public abstract boolean isChanged();
public abstract E getValue();
public abstract void setValue(E newValue);
private void initComponents() {
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 400, Short.MAX_VALUE)
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 300, Short.MAX_VALUE)
);
}
}
With that created to your satisfaction, we need to place it in a pop-up menu:
package complexcombobox;
import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
/**
*
* @author MaskedCoder
*
* @param E - the object to be edited
* @param P - the extension of AbstractPopupPanel that will display the object as you wish
*/
public class ComplexPopup<E, P extends AbstractPopupPanel<E>> extends JPopupMenu {
/*
* Interface to notify user of editing
*/
public interface EditListener<E> {
// return the object's value to be edited.
// if the user returns null, the existing Dice is used.
public E beforeEdit();
// receives the new value
// called *only* when the object has actually changed.
public void afterEdit(E newValue);
}
/*
*internal variables
*/
private P thisPanel;
private EditListener<E> userListener;
// private Class<E> eClass;
// private Class<P> pClass;
private PopupMenuListener myPopupListener = new PopupMenuListener() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
if(userListener != null) {
E tmpSize = userListener.beforeEdit();
if(tmpSize != null) thisPanel.setValue(tmpSize);
}
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
if(userListener != null) {
// if(thisPanel.isChanged())
userListener.afterEdit((E) thisPanel.getValue());
}
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
popupMenuWillBecomeInvisible(e);
}
};
/*
* Constructors
*/
public ComplexPopup(E iniValue, P iniDropdownPanel, EditListener<E> iniListener) {
super();
init(iniValue, iniDropdownPanel, iniListener);
}
private void init(E iniValue, P iniDropdownPanel, EditListener<E> iniListener) {
thisPanel = iniDropdownPanel;
thisPanel.setValue(iniValue);
add(thisPanel);
userListener = iniListener;
this.addPopupMenuListener(myPopupListener);
}
/*
* Public Properties
*/
public E getValue() {
return (E) thisPanel.getValue();
}
public void setValue(E newObjectSize) {
thisPanel.setValue(newObjectSize);
}
public EditListener getUserListener() {
return userListener;
}
public void setEditListener(EditListener newListener) {
userListener = newListener;
}
/*
* functional extensions
*/
public void afterEdit(E newValue) {
if(userListener != null) {
if(thisPanel.isChanged())
userListener.afterEdit((E) thisPanel.getValue());
}
}
}
This places the form in a JPopupMenu
and allows access to the object to be edited as well as notification before and after editing. Now, I couldn't co-opt the JComboBox
interface without getting deeper into the mechanics than I wanted to go, so I fudged the combo box, as I mentioned:
package complexcombobox;
/**
*
* @author MaskedCoder
*
* @param <I> the Item (Object) to be edited
* @param <Q> the AbstractPopupPanel to be used to do the editing.
*/
public class ComplexComboBox<I, Q extends AbstractPopupPanel<I>> extends javax.swing.JPanel {
private ComplexPopup<I, Q> thePopup;
private ComplexPopup.EditListener<I> myListener = new ComplexPopup.EditListener<I>() {
@Override
public I beforeEdit() {
return null; // no changes so, just let it ride.
}
@Override
public void afterEdit(I newValue) {
txtEditor.setText(thePopup.getValue().toString());
}
};
/**
* Creates new form ObjectSizeComboBox
*/
public ComplexComboBox(I iniValue, Q iniPanel) {
initComponents();
thePopup = new ComplexPopup<I, Q>(iniValue, iniPanel, myListener);
}
public I getValue() {
return thePopup.getValue();
}
public void setValue(I newValue) {
thePopup.setValue(newValue);
}
private void initComponents() {
txtEditor = new javax.swing.JTextField();
btnShowPanel = new javax.swing.JButton();
setBackground(new java.awt.Color(255, 255, 255));
setMinimumSize(new java.awt.Dimension(40, 19));
txtEditor.setEditable(false);
txtEditor.setToolTipText("");
txtEditor.setOpaque(false);
btnShowPanel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/testpopupsubform/art/dropdownarrow.png"))); // NOI18N
btnShowPanel.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
btnShowPanelActionPerformed(evt);
}
});
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addComponent(txtEditor, javax.swing.GroupLayout.DEFAULT_SIZE, 115, Short.MAX_VALUE)
.addGap(1, 1, 1)
.addComponent(btnShowPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 24, javax.swing.GroupLayout.PREFERRED_SIZE))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(btnShowPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(txtEditor)
);
}
private void btnShowPanelActionPerformed(java.awt.event.ActionEvent evt) {
thePopup.show(txtEditor, 0, txtEditor.getHeight());
}
private javax.swing.JButton btnShowPanel;
private javax.swing.JTextField txtEditor;
}
This seems to work very well in a cell editor for a table, provided you make the row heights tall enough. That's no different than a regular JComboBox
editor, though:
package complexcombobox;
import java.awt.Component;
import javax.swing.AbstractCellEditor;
import javax.swing.JTable;
import javax.swing.table.TableCellEditor;
/**
*
* @author MaskedCoder
*
* @param <E> the Element (Object) to be edited
* @param <P> the AbstractPopupPanel to be used to do the editing.
*/
public class ComplexCellEditor<E, P extends AbstractPopupPanel<E>> extends AbstractCellEditor
implements TableCellEditor {
private ComplexComboBox<E, P> ComplexEditor;
/*
* Constructors
*/
public ComplexCellEditor(E iniValue, P iniPanel) {
super();
ComplexEditor = new ComplexComboBox<E, P>(iniValue, iniPanel);
}
/*
* AbstractCellEditor Extensions
*/
@Override
public boolean stopCellEditing() {
return super.stopCellEditing();
}
/*
* TableCellEditor Implementation
*/
@Override
public E getCellEditorValue() {
return ComplexEditor.getValue();
}
@Override
public Component getTableCellEditorComponent(JTable iniTable, Object iniValue, boolean isSelected, int row, int column) {
ComplexEditor.setValue((E) iniValue);
return ComplexEditor;
}
}
Three things I'm not entirely satisfied with: - I'd like to grab the JComboBox
down-arrow icon from the system rather than relying on a project resource - I've had to pass through the AbstractPopupPanel
instance which means that the coder has to create the panel themselves and then hand it to the constructor for ComplexComboBox
or ComplexCellEditor
. I'd rather have created the panel internally, in ComplexPopup
and managed it myself. I ran into difficulty instantiating Generic constructors. Pass-through does allow the coder more control but I don't think that's needed and I'd like it to be optional at least.
My knowledge of Generics isn't advanced but this seems to do the trick.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.