Intercepting drag and drop events in a compound component? - java

Say I have a JPanel containing some JTextFields. I would like to perform the same drop action for this JPanel and its children. In other words, I would like the drop action onto the children to be treated the same way as a drop action onto the JPanel.
Is there any other way other than setting the same DropTargetListener for the JPanel and its children?
I know that if I set the TransferHandler of those JTextFields to null, the JPanel will receive the drag and drop event. However, this will destroy the copy and paste functionality of the textfield.
I know that I can intercept mouse events with JLayer. Is there something like this for drag events?
In the end, I added listeners separately to the child components. Because I needed the drop location relative to the parent as well, I used SwingUtilities.convertToPoint() separately on the child components. Which means a lot of different listeners used -- more memory usage. But seems to be the best way for now.

If you want to be able to drop items on a panel but you want any components on the panel to be ignored for dropping purposes you can deactivate the drop target on each of the components added to the panel. You will still be able to cut and paste within them, and there is even a way to initiate a drag from them, but you won't be able to drop anything on them - the drop event goes straight through them to the drop target associated with the panel.
To do this, simply call
component.getDropTarget().setActive(false);
for each component on the panel.
I found this useful when building a calendar panel where I wanted to be able to drag appointments around but drop them on the panel even if it was (partially or completely) covered in other appointments.

Not sure if this is what you had in mind, but, I basically added the same DropTargetListener to all of my components, which meant that it didn't matter where I dragged/dropped the incoming request, all the components triggered the same events...
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class TestDragNDrop100 {
public static void main(String[] args) {
new TestDragNDrop100();
}
public TestDragNDrop100() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel implements DropTargetListener {
public TestPane() {
DropTarget dt = new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, this, true);
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.HORIZONTAL;
for (int y = 0; y < 4; y++) {
gbc.gridy = y;
for (int x = 0; x < 4; x++) {
gbc.gridx = x;
JTextField field = new JTextField(10);
DropTarget child = new DropTarget(field, DnDConstants.ACTION_COPY_OR_MOVE, this, true);
add(field, gbc);
}
}
}
#Override
public void dragEnter(DropTargetDragEvent dtde) {
System.out.println("DragEnter - " + dtde.getDropTargetContext().getComponent());
}
#Override
public void dragOver(DropTargetDragEvent dtde) {
System.out.println("DragOver - " + dtde.getDropTargetContext().getComponent());
}
#Override
public void dropActionChanged(DropTargetDragEvent dtde) {
System.out.println("dropActionChanged" + dtde.getDropTargetContext().getComponent());
}
#Override
public void dragExit(DropTargetEvent dte) {
System.out.println("dragExit" + dte.getDropTargetContext().getComponent());
}
#Override
public void drop(DropTargetDropEvent dtde) {
System.out.println("drop" + dtde.getDropTargetContext().getComponent());
}
}
}
I should also note. I tested the fields cut/copy/paste functionality and had no issues.

Implement a custom TransferHandler on the container which delegates to its children as appropriate, something along the lines of:
for (int i = 0; i < 5; i++) {
parent.add(new JTextField("item" + i, 20));
};
TransferHandler handler = new TransferHandler() {
#Override
public boolean canImport(TransferSupport support) {
TransferHandler childHandler = getTargetHandler();
return childHandler.canImport(
getTargetSupport(support));
}
protected TransferSupport getTargetSupport(TransferSupport support) {
return new TransferSupport(getTarget(), support.getTransferable());
}
protected TransferHandler getTargetHandler() {
return getTarget().getTransferHandler();
}
protected JComponent getTarget() {
return (JComponent) parent.getComponent(0);
}
#Override
public boolean importData(TransferSupport support) {
return getTargetHandler().importData(getTargetSupport(support));
}
};
parent.setTransferHandler(handler);

Related

JFrame show labels in a scroll panel

I'm making a frame who needs to show labels in a Scroll Panel, but after I add the labels the scroll don't work.
JScrollPane scrollPane = new JScrollPane();
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setBounds(1, 1, 210, 259);
panel.add(scrollPane);
JPanel roomList = new JPanel();
scrollPane.setViewportView(roomList);
roomList.setLayout(null);
int x=0;
for(String l : list) {
JLabel c = new JLabel(l+" "+x);
c.setBounds(new Rectangle(1, 1+x*11, 191, 14));
roomList.add(c);
x++;
}
I'm sure the list has more than 22.
I don't know how to google it!
Your basic problem is, you don't understand how the layout management API works, or how to replace it's functionality when you choose to discard it.
You problem starts here:
roomList.setLayout(null);
There's a lot of work going on in the background which provides a great deal of information to various parts of the API, while on the surface, the layout management API is not complex, the role it plays is.
The JScrollPane will use the component's preferredSize to determine when it should display the scrollbars. Since you've done away with this automated calculation, the JScrollPane has nothing to go on
For more information, have a look at Laying Out Components Within a Container
As a simple example...
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Rectangle;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.Scrollable;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JScrollPane scrollPane = new JScrollPane(new TestPane());
frame.add(scrollPane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel implements Scrollable {
public TestPane() {
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
for (int index = 0; index < 100; index++) {
add(new JLabel("Row " + index), gbc);
gbc.gridy++;
}
}
#Override
public Dimension getPreferredScrollableViewportSize() {
return new Dimension(100, 50);
}
#Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
return 32;
}
#Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
return 32;
}
#Override
public boolean getScrollableTracksViewportWidth() {
return getPreferredSize().width <= getWidth();
}
#Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
}
}
This example implements the Scrollable interface, this is not always required, but is used, for this example, to provide a hint to the JScrollPane about the preferred size of the viewable area it should use, otherwise it will attempt to use the component's preferredSize.
But, as has already been suggested, there are other, simpler and more optimised solutions available to you.
If your information is simple enough, you can use a JList to list a number of values in a vertical manner, see How to use Lists for more details.
If you information is in a more complex structure, you could use a JTable, which provides a row and column style structure. See How to use tables for more information
Have you tried using jLists instead of JScrollPanes ?
They're very easily implemented, look great and work like a charm.
DefaultListModel model = new DefaultListModel();
for(String l : list) {
model.addElement(l);
}
yourList.setModel(model);
Where list is the list with the room data and yourList is the jList.

Changing Look And Feel by Swapping UIDefaults

I am writing a GUI Builder and want the user to be able to change the LookAndFeel of the GUI he builds. The LookAndFeel should only be changed for the Components inside the editor area. The rest of the Application should remain with the SystemLookAndFeel.
The great problem is, that the LookAndFeel is implemented as a Singleton and changing the LookAndFeel multiple times during the Application causes a lot of bugs.
I started experimenting with Buttons:
I tried setting the ButtonUI to MetalButtonUI, but they didn't render properly. So I debugged the default paintComponent method of JButton and saw that the ButtonUI still needed the UIDefaults, which were not complete since they were the WindowsUIDefaults.
My current solution is to set the MetalLookAndFeel, save the UIDefaults, then change the LookAndFeel to SystemLookAndFeel and save those UIDefaults aswell and everytime I draw a Button inside the editor I swap the UIDefaults.
Here is the Code:
public class MainClass{
public static Hashtable systemUI;
public static Hashtable metalUI;
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
metalUI = new Hashtable();
metalUI.putAll(UIManager.getDefaults());
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
systemUI = new Hashtable();
systemUI.putAll(UIManager.getDefaults());
} catch (Exception e) {
e.printStackTrace();
}
/* ...
* Generate JFrame and other stuff
* ...
*/
}
});
}
}
public class MyButton extends JButton {
public MyButton(String text) {
super(text);
ui = MetalButtonUI.createUI(this);
}
#Override public synchronized void paintComponent(Graphics g) {
UIManager.getDefaults().putAll(Application.metalUI);
super.paintComponent(g);
UIManager.getDefaults().putAll(Application.systemUI);
}
}
As you can see here the result is pretty good. On the left is the MetalLaF as it should look and on the right, how it gets rendered in my application. The gradient is painted correctly, but the Border and the Font aren't.
So I need to know why not all elements of the LaF are beeing applied to the Button and how to fix that.
-
Edit:
I found an ugly solution. The LookAndFeel has to be changed before Button creation, because the Graphics object will be created in the Constructor. After the super constructor was called you can change the LookAndFeel back.
Next you need to change the LookAndFeel before the Component is painted/repainted. The only point I got it working was in paintComponent before super is called. You can change it back after super is called.
Code:
import javax.swing.*;
import javax.swing.plaf.metal.MetalButtonUI;
import java.awt.*;
public class MainClass {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
e.printStackTrace();
}
JFrame f = new JFrame("Test");
f.setDefaultCloseOperation(f.getExtendedState() | JFrame.EXIT_ON_CLOSE);
try {
UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
} catch (Exception ex) {
}
f.setLayout(new FlowLayout());
f.add(new MyButton("MetalButton"));
f.add(new JButton("SystemButton"));
f.pack();
f.setVisible(true);
}
});
}
}
class MyButton extends JButton {
public MyButton(String text) {
super(text);
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
}
ui = MetalButtonUI.createUI(this);
}
#Override public synchronized void paintComponent(Graphics g) {
try {
UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
super.paintComponent(g);
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
}
}
}
Edit 2:
Do not use this unless absolutely necessery!
This is extremely unstable. After a lot of testing I found it less buggy when I just swapped the UIDefaults instead of the whole LookAndFeel, but I do not recommend doing any of those.
Edit 3:
The best solution I found was using JavaFX as a GUI. I inserted a swing node into the Editor area and now can modify the Look and Feel of the swing components as often as I want without any noticeable side effects.
Rant:
If you can always choose JavaFX if you want to modify the style of your application. CSS makes it as easy as possible without any side effects ever!
Much Thanks
Jhonny
Disclarimer
Swing's Look And Feel isn't designed to be switched after it's first initalised, it's actually a kind of fluky side effect that it's possible. Some look and feels and some components might not like you doing this and may not behave as they might other wise under normal conditions.
Possible solution
For the love of sanity, DON'T change the UI defaults in the paintComponent method (don't change the state of the UI at all from within any paint method EVER, painting paints the current state only), that's just asking for no end of trouble.
Instead, when required use UIManager.setLookAndFeel(,,,) and SwingUtiltiies#updateComponentTreeUI and pass in the most top level container
For example...
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class LookAndFeelSwitcher {
public static void main(String[] args) {
new LookAndFeelSwitcher();
}
public LookAndFeelSwitcher() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(2, 2, 2, 2);
add(new JLabel("I have a bad feeling about this"), gbc);
add(new JTextField("When this blows up in your face, don't blame me"), gbc);
UIManager.LookAndFeelInfo[] lafs = UIManager.getInstalledLookAndFeels();
DefaultComboBoxModel model = new DefaultComboBoxModel(lafs);
JComboBox cb = new JComboBox(model);
cb.setRenderer(new LookAndFeelInfoListCellRenderer());
add(cb, gbc);
String name = UIManager.getLookAndFeel().getName();
for (int index = 0; index < model.getSize(); index++) {
UIManager.LookAndFeelInfo info = (UIManager.LookAndFeelInfo) model.getElementAt(index);
if (info.getName().equals(name)) {
model.setSelectedItem(info);
break;
}
}
cb.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
UIManager.LookAndFeelInfo info = (UIManager.LookAndFeelInfo) cb.getSelectedItem();
String className = info.getClassName();
try {
UIManager.setLookAndFeel(className);
SwingUtilities.updateComponentTreeUI(SwingUtilities.windowForComponent(TestPane.this));
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
}
});
}
public class LookAndFeelInfoListCellRenderer extends DefaultListCellRenderer {
#Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
if (value instanceof UIManager.LookAndFeelInfo) {
UIManager.LookAndFeelInfo info = (UIManager.LookAndFeelInfo) value;
value = info.getName();
}
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
}
}
}

CellRenderer Item repaint

I created my own CellRenderer which include some several strings and a JProgressBar in a JList Item ... But the JProgressBar and so the whole JList Item will painted one time and I'm looking for a way to repaint the Items ... I tried to start a thread, that will permanently repaint ... But I don't know what I have to repaint to get the result ...
JList repaint ... no result
CellRenderer repaint ... no result
JFrame repaint ... no result
Does anyone understand my Problem and know a way out?
Thank you very much!
UPDATE: [Update deleted]
NEXT UPDATE:
import java.awt.Component;
import java.awt.Dimension;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.ListCellRenderer;
public class Main extends JFrame{
public DefaultListModel contentListModel = new DefaultListModel();
public MyCellRenderer MCR = new MyCellRenderer();
public JList contentList = new JList(contentListModel);
public Main(){
super("Example");
setMinimumSize(new Dimension(300,300));
setDefaultCloseOperation(EXIT_ON_CLOSE);
contentList.setCellRenderer(MCR);
contentListModel.addElement("");
contentListModel.addElement("");
add(contentList);
}
public static void main(String[] args){
new Main().setVisible(true);
}
class MyCellRenderer extends JPanel implements ListCellRenderer{
public MyCellRenderer(){
JProgressBar jpb = new JProgressBar();
jpb.setIndeterminate(true);
add(jpb);
}
#Override
public Component getListCellRendererComponent(JList arg0, Object arg1,
int arg2, boolean arg3, boolean arg4) {
// TODO Auto-generated method stub
return this;
}
}
}
As already mentioned, renderers are not live components, that is not part of component hierarchy. Consequently, "natural" animation effects (like f.i. the movement of the indeterminate progressbar) are lost. A trick that might work - but beware: that's highly LAF dependent! - is to lie to the system and report the bar as being dispayable always. That combined with a timer faking a new value every x ms might show the animation again:
public static class ProgressBarRenderer implements TableCellRenderer {
/** The bar. */
private JProgressBar indeterminate = new JProgressBar() {
// fake displayable to trigger ui animation
#Override
public boolean isDisplayable() {
return true;
};
};
/** The bar. */
private JProgressBar determinate = new JProgressBar() ;
public ProgressBarRenderer() {
indeterminate.setStringPainted(true);
indeterminate.setIndeterminate(true);
indeterminate.setString(null);
}
#Override
public Component getTableCellRendererComponent(JTable table,
Object value, boolean isSelected, boolean hasFocus, int row,
int column) {
int pbi = (Integer) value;
if (pbi < 0) {
return indeterminate;
}
determinate.setValue(pbi);
return determinate;
}
}
// a timer driving the animation
Action update = new AbstractAction() {
int count;
#Override
public void actionPerformed(ActionEvent e) {
table.setValueAt(-1, 0, AncientSwingTeam.INTEGER_COLUMN);
}
};
new Timer(100, update).start();
Cell renderers are static/rubber stamps of components, they are not "real", active components. They are simply "painted" onto the surface of the view that uses them.
This means that it's not (really) possible to repaint them as such. You can encourage the list to update by changing the value of the row you want to change. This will cause the model to trigger an update that will cause the list to repaint.
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class TestProgressListCellRenderer {
public static void main(String[] args) {
new TestProgressListCellRenderer();
}
public TestProgressListCellRenderer() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
final DefaultListModel<Float> model = new DefaultListModel<>();
model.addElement(0f);
JList<Float> list = new JList<>(model);
list.setCellRenderer(new ProgressListCellRenderer());
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new JScrollPane(list));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
Timer timer = new Timer(125, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
float value = model.getElementAt(0);
value += 0.01;
if (value >= 1f) {
value = 1f;
((Timer)e.getSource()).stop();
}
model.setElementAt(value, 0);
}
});
timer.setRepeats(true);
timer.setCoalesce(true);
timer.start();
}
});
}
public class ProgressListCellRenderer extends JProgressBar implements ListCellRenderer<Float> {
#Override
public Component getListCellRendererComponent(JList<? extends Float> list, Float value, int index, boolean isSelected, boolean cellHasFocus) {
setValue(Math.round(value * 100));
return this;
}
}
}
This is a simple example and I tried using setIndeterminate, but I couldn't get it to update (very annoying), but this might trigger some ideas
+1 for interesting issue,
but Renderer in Swing isn't designated to showing animations, this is only static painting, illusion, snapshot,
you have to animate this Object in your code, but not easy job for JProgressBar.setIndeterminate(true); if is possible without dirty hacks,
required own JProgressBar
required own animation for ListCellRenderer, and the result could be so far in compare with JProgressBar placed in JPanel e.g.
I think JList couldn't be proper JComponents, use JTable with one Column and /or without JTableHeader
simple workaround to use JPanel (layed by GridLayout) with JProgressBars for JProgressBar.setIndeterminate(true);, then output to the Swing GUI will be very similair to the JTable (without JTableHeader or JList)
put this JPanel to JScrollPane
override getPreferredSize for JScrollPane, then you'll simulating JList.setVisibleRowCount properly
change JScrollBar.setUnitIncrement for natural scrolling for JPanel with JComponents and with JList, JTable, JTextArea placed in JScrollPane
You may be able to drive the animation with your own javax.swing.Timer in a custom ProgressBarUI, seen here; incrementAnimationIndex() looks promising, but I've not tried it.
Absent a better solution, you can use a ProgressIcon in a DefaultListCellRenderer; the approach is illustrated here in a JTabbedPane.
It's easy.
public void updateListData(final JList myList) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
ListModel lm = myList.getModel();
DefaultListModel dlm = new DefaultListModel();
for (int i = 0; i < lm.getSize(); i++) {
dlm.addElement(lm.getElementAt(i));
}
myList.setModel(dlm);
}
});
}
The code above can be called from EDT and from another thread. But you also should read how to deal with Swing components and understand models (ListModel, TableModel, etc). To ask an element in JList to repaint you should modify its object in model.

Implementing drag-and-drop from one JPanel to another

I'm new to drag-and-drop in Swing. I have a JPanel that draws an image with a caption superimposed on it. I want to implement drag and drop on this JPanel, but after going through some documentation and tutorials I didn't find any usable pointers on how it's done for this type of component. For starters, it doesn't have a setDragEnabled function.
Can I make a JPanel draggable? I want to use this DnD maneuver to pass a reference to a certain object from one panel to another.
May be this can help you.
Drag and Drop of complex custom objects in Java
You can implement drag-and-drop behavior on any JComponent. See the setTransferHandler method.
The setDragEnabled method is typically provided on components where a good default D&D behavior can be implemented in the JDK. In such cases you can just activate the default D&D by calling that method.
On a JPanel they (=the Swing developers) could probably not think of any decent default D&D behavior, so you will have to implement your own TransferHandler. I strongly suggest to read the Drag-and-drop tutorial before starting
I don't know how viable sounds but when I needed to drag and drop panels I did it this way:
Firstable I implemented action events for dragable panels and containers, it can be both
I used a static variables for selected parent, selected child and current panel
when the mouse is over a panel you set it as the current panel
when you click , mouse down, whatever, you check if currentpanel is the clicked one and set is as child panel
when the mouse is over a panel and child panel is not null, then it seems that you're dragging, current panel will turn into parent panel once you release the mouse
you have to add some validation.
If a panel is being dragged you can use your own implementation, it could be follow the mouse coords or just highlight it and highlight the parent, I used this last option to simulate the drag
ok I wrote this, is so buggy but this is the idea:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JDesktopPane;
import javax.swing.JFrame;
import javax.swing.JTextField;
/**
*
* #author porfiriopartida
*/
public class DraggablePanel extends JDesktopPane implements ContainerPanel{
public ContainerPanel parent;
static DraggablePanel over;
static ContainerPanel overParent;
static DraggablePanel dragging;
static ContainerPanel draggingParent;
public DraggablePanel(){
this(null);
}
public DraggablePanel(ContainerPanel parent){
this.parent = parent;
setBorder(BorderFactory.createLineBorder(Color.black));
setBounds(0,0,100,100);
if(parent != null)
addMouseListener(new MouseAdapter(){
#Override
public void mouseEntered(MouseEvent me) {
DraggablePanel src = (DraggablePanel) me.getSource();
DraggablePanel.over = src;
DraggablePanel.overParent = DraggablePanel.over.parent;
}
#Override
public void mouseExited(MouseEvent me) {
}
#Override
public void mouseReleased(MouseEvent me) {
if(DraggablePanel.over != null && DraggablePanel.dragging != null && DraggablePanel.overParent != null){
Rectangle bounds = DraggablePanel.dragging.getBounds();
bounds.x = me.getX();
bounds.y = me.getY();
//Remove child from parent
DraggablePanel.dragging.parent.removePanel(DraggablePanel.dragging);
if(DraggablePanel.dragging.parent != DraggablePanel.overParent){
//add child to new parent
DraggablePanel.overParent.addPanel(DraggablePanel.dragging, bounds);
}
else{
//same parent selected
DraggablePanel.dragging.parent.addPanel(DraggablePanel.dragging, bounds);
};
DraggablePanel.dragging.parent = DraggablePanel.overParent;
}
//cleaning variables
DraggablePanel.dragging = null;
DraggablePanel.over = null;
DraggablePanel.draggingParent = null;
DraggablePanel.overParent = null;
}
#Override
public void mousePressed(MouseEvent me) {
DraggablePanel.dragging = (DraggablePanel) me.getSource();
DraggablePanel.draggingParent = DraggablePanel.dragging.parent;
}
});
addMouseListener(new MouseAdapter() {
#Override
public void mouseEntered(MouseEvent me) {
super.mouseEntered(me);
ContainerPanel src = (ContainerPanel) me.getSource();
DraggablePanel.overParent = src;
if (DraggablePanel.draggingParent == null || DraggablePanel.draggingParent == src) {
setBorder(BorderFactory.createLineBorder(Color.BLACK, 2));
} else {
setBorder(BorderFactory.createLineBorder(Color.blue, 2));
}
}
#Override
public void mouseExited(MouseEvent me) {
}
#Override
public void mouseReleased(MouseEvent me) {
}
});
}
#Override
public void addPanel(DraggablePanel panel) {
panel.parent = this;
add(panel);
repaint();
revalidate();
try {
getParent().repaint();
} catch (Exception e) {
}
}
#Override
public void addPanel(DraggablePanel panel, Rectangle bounds) {
setBounds(bounds);
addPanel(panel);
}
#Override
public void removePanel(DraggablePanel panel) {
remove(panel);
}
public static void main(String args[]){
JFrame frame = new JFrame();
frame.setLayout(new GridLayout(1,1));
JTextField tf = new JTextField("textfield");
JButton btn = new JButton("Button");
DraggablePanel desktop = new DraggablePanel();
frame.add(desktop);
DraggablePanel p1 = new DraggablePanel(desktop);
p1.setLayout(new GridLayout(2,1));
p1.add(tf);
p1.setBounds(0,0,100,50);
tf.setBounds(5,5,80,30);
DraggablePanel p2 = new DraggablePanel(desktop);
p2.setLayout(new GridLayout(2,1));
p2.add(btn);
p2.setBounds(50,50,50,30);
btn.setBounds(5,5,30,20);
desktop.add(p1);
desktop.add(p2);
frame.setPreferredSize(new Dimension(600,400));
frame.pack();
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}

How do I make JScrollPane scroll to follow input focus?

I have a Swing app with a large panel which is wrapped in a JScrollPane. Users normally move between the panel's subcomponents by tabbing, so when they tab to something out view, I want the scroll pane to autoscroll so the component with input focus is always visible.
I've tried using KeyboardFocusManager to listen for input focus changes, and then calling scrollRectToVisible.
Here's an SSCCE displaying my current strategy (just copy/paste and run!):
import java.awt.KeyboardFocusManager;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
public class FollowFocus {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final int ROWS = 100;
final JPanel content = new JPanel();
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
content.add(new JLabel(
"Thanks for helping out. Use tab to move around."));
for (int i = 0; i < ROWS; i++) {
JTextField field = new JTextField("" + i);
field.setName("field#" + i);
content.add(field);
}
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.addPropertyChangeListener("focusOwner",
new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (!(evt.getNewValue() instanceof JComponent)) {
return;
}
JComponent focused = (JComponent) evt.getNewValue();
if (content.isAncestorOf(focused)) {
System.out.println("Scrolling to " + focused.getName());
focused.scrollRectToVisible(focused.getBounds());
}
}
});
JFrame window = new JFrame("Follow focus");
window.setContentPane(new JScrollPane(content));
window.setSize(200, 200);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
});
}
}
If you run this example, you'll notice it doesn't work very well. It does get the focus change notifications, but the call to scrollRectToVisible doesn't appear to have any effect. In my app (which is too complex to show here), scrollRectToVisible works about half the time when I tab into something outside of the viewport.
Is there an established way to solve this problem? If it makes any difference, the Swing app is built on Netbeans RCP (and most of our customers run Windows).
My comment to the other answer:
scrollRectToVisible on the component itself is the whole point of that
method ;-) It's passed up the hierarchy until a parent doing the
scroll is found
... except when the component itself handles it - as JTextField does: it's implemented to scroll horizontally to make the caret visible. The way out is to call the method on the field's parent.
Edit
just for clarity, the replaced line is
content.scrollRectToVisible(focused.getBounds());
you have to take Rectangle from JPanel and JViewPort too, then compare, for example
notice (against down-voting) for final and nice output required some work for positions in the JViewPort
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
//http://stackoverflow.com/questions/8245328/how-do-i-make-jscrollpane-scroll-to-follow-input-focus
public class FollowFocus {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final int ROWS = 100;
final JPanel content = new JPanel();
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
content.add(new JLabel(
"Thanks for helping out. Use tab to move around."));
for (int i = 0; i < ROWS; i++) {
JTextField field = new JTextField("" + i);
field.setName("field#" + i);
content.add(field);
}
final JScrollPane scroll = new JScrollPane(content);
KeyboardFocusManager.getCurrentKeyboardFocusManager().
addPropertyChangeListener("focusOwner", new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (!(evt.getNewValue() instanceof JComponent)) {
return;
}
JViewport viewport = (JViewport) content.getParent();
JComponent focused = (JComponent) evt.getNewValue();
if (content.isAncestorOf(focused)) {
System.out.println("Scrolling to " + focused.getName());
Rectangle rect = focused.getBounds();
Rectangle r2 = viewport.getVisibleRect();
content.scrollRectToVisible(new Rectangle(rect.x, rect.y, (int) r2.getWidth(), (int) r2.getHeight()));
}
}
});
JFrame window = new JFrame("Follow focus");
window.setContentPane(new JScrollPane(content));
window.setSize(200, 200);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
});
}
}
Here my short summary.
Add this to your Tools class:
public static void addOnEnter(Component c, Consumer<FocusEvent> onEnter) {
FocusListener fl = new FocusListener() {
#Override
public void focusGained(FocusEvent e) {
onEnter.accept(e);
}
#Override
public void focusLost(FocusEvent e) { }
};
c.addFocusListener(fl);
}
public static void scrollToFocus(FocusEvent e) {
((JComponent) e.getComponent().getParent()).scrollRectToVisible(
e.getComponent().getBounds());
}
and use it like this:
Tools.addOnEnter(component, Tools::scrollToFocus);
component can be JTextField, JButton, ...
One major issue in your code is:
focused.scrollRectToVisible(focused.getBounds());
You are calling scrollRectToVisible on the component itself! Presumably a typo.
Make your JScrollPane a final variable and call
scrollPane.getViewport().scrollRectToVisible(focused.getBounds());
Here jtextbox is the component you want to focus and jscrollpane is your scrollpane:
jScrollpane.getVerticalScrollBar().setValue(jtextbox.getLocation().x);

Categories

Resources