I've implemented a JLayer<JPanel> component which paint a zoomed Graphics of itself, so all his descending components will be zoomed too.
This JLayer is applied as ContentPane to a JFrame component.
The main problem is that all the zoom applies, indeed, only to the graphic and the actual size and position of the components remain the same. This implies that all the mouse events happen in the wrong position respectively to what the user see.
I've king of tweaked it defining a GlassPane JComponent at the top of the frame which has it's own MouseInputAdapter which redispatch the MouseEvents to the underlying components using SwingUtilities.getDeepestComponentAt(). This is accomplished by creating a new MouseEvent with the mouse coordinates mapped depending on the zoom value. (obtained modifying the How to use RootPanes tutorial)
This method is obviously not satisfying because a lot of events simply can't be fired (for example the MOUSE_ENTERED events fired by all the descending components).
On the other hand using the LayerUI.paint() override implies that i have to have something which remap all the mouse coordinates.
Is there a way to map the mouse coordinates without breaking the inner MouseEvent processing?
or
Is there another way to zoom in the bitmap of the components while modifying also the real position and size? I've kind of tried this but calling the setSize() of an inner component seems to someway call the LayerUi.paint() a second time so all that i get is some bigger graphic disconnected from the actual widget position
This is what i've done so far thanks to MadProgrammer and PBar extensions, working with JLayer in java 8.
The TransformUI include in itself the PBar's org.pbjar.jxlayer.plaf.ext.MouseEventUI and org.pbjar.jxlayer.plaf.ext.TransformUI:
package jzoom.transform;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JComponent;
import javax.swing.JLayer;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.plaf.LayerUI;
/**
*
* This UI apply an {#link AffineTransform} to all the visible objects, and apply the inversion
* of the same transform to all the mouse event's coordinates
*
* #author Andrea.Maracci based on the pbjar JXLayer extension
*
*/
public class TransformUI extends LayerUI<JComponent> {
private static final long serialVersionUID = 1L;
private Component lastEnteredTarget, lastPressedTarget;
private final Set<JComponent> originalDoubleBuffered = new HashSet<JComponent>();
private AffineTransform transform = new AffineTransform();
private JLayer<JComponent> installedLayer;
private boolean dispatchingMode = false;
/**
* Process the mouse events and map the mouse coordinates inverting the internal affine transformation.
*
* #param
* event the event to be dispatched
* layer the layer this LayerUI is set to
*
*/
#Override
public void eventDispatched(AWTEvent event, final JLayer<? extends JComponent> layer) {
if (event instanceof MouseEvent) {
MouseEvent mouseEvent = (MouseEvent) event;
/*
* The if discriminate between generated and original event.
* Removing it cause a stack overflow caused by the event being redispatched to this class.
*/
if (!dispatchingMode) {
// Process an original mouse event
dispatchingMode = true;
try {
redispatchMouseEvent(mouseEvent, layer);
} finally {
dispatchingMode = false;
}
} else {
/*
* Process generated mouse events
* Added a check, because on mouse entered or exited, the cursor
* may be set to specific dragging cursors.
*/
if (MouseEvent.MOUSE_ENTERED == mouseEvent.getID() || MouseEvent.MOUSE_EXITED == mouseEvent.getID()) {
layer.getGlassPane().setCursor(null);
} else {
Component component = mouseEvent.getComponent();
layer.getGlassPane().setCursor(component.getCursor());
}
}
} else {
super.eventDispatched(event, layer);
}
layer.repaint();
}
/**
* Set the affine transformation applied to the graphics
* #param transform the transformation
*/
public void setTransform(AffineTransform transform) {
if (transform != null) {
this.transform = transform;
}
}
/**
* Return the affine transformation applied to the graphics
* #return the transformation
*/
public AffineTransform getTransform() {
return transform;
}
/**
* Paint the specified component {#code c} applying the transformation on it's graphic
*
* #param
* g - the Graphics context in which to paint
* c - the component being painted
*/
#Override
public void paint(Graphics g, JComponent c) {
if (g instanceof Graphics2D) {
Graphics2D g2 = (Graphics2D) g.create();
JLayer<? extends JComponent> l = (JLayer<? extends JComponent>) c;
g2.transform(transform);
paintLayer(g2, l);
g2.dispose();
}
}
/**
* Paint the view decorated by the JLayer {#code layer} and the JLayer itself
*
* #param g2
* #param layer the layer this LayerUI is set to
*/
private final void paintLayer(Graphics2D g2, JLayer<? extends JComponent> layer) {
JComponent view = layer.getView();
if (view != null) {
if (view.getX() < 0 || view.getY() < 0) {
setToNoDoubleBuffering(view);
g2.translate(view.getX(), view.getY());
view.paint(g2);
for (JComponent jComp : originalDoubleBuffered) {
jComp.setDoubleBuffered(true);
}
originalDoubleBuffered.clear();
return;
}
}
layer.paint(g2);
}
/**
* Disable the double buffering for the {#code component} and for all of it's children
*
* #param component
*/
private void setToNoDoubleBuffering(Component component) {
if (component instanceof JComponent) {
JComponent jComp = (JComponent) component;
if (jComp.isDoubleBuffered()) {
originalDoubleBuffered.add(jComp);
jComp.setDoubleBuffered(false);
}
}
if (component instanceof Container) {
Container container = (Container) component;
for (int index = 0; index < container.getComponentCount(); index++) {
setToNoDoubleBuffering(container.getComponent(index));
}
}
}
/**
* {#inheritDoc}
*/
#Override
public void uninstallUI(JComponent component) {
if (!(component instanceof JLayer<?>)) {
throw new IllegalArgumentException(
this.getClass().getName() + " invalid class, must be a JLayer component");
}
JLayer<JComponent> jlayer = (JLayer<JComponent>) component;
jlayer.setLayerEventMask(0);
super.uninstallUI(component);
}
/**
* {#inheritDoc}
*/
#Override
public void installUI(JComponent component) throws IllegalStateException {
super.installUI(component);
if (installedLayer != null) {
throw new IllegalStateException(this.getClass().getName() + " cannot be shared between multiple layers");
}
if (!(component instanceof JLayer<?>)) {
throw new IllegalArgumentException(
this.getClass().getName() + " invalid class, must be a JLayer component");
}
// component.getClass().getDeclaringClass();
installedLayer = (JLayer<JComponent>) component;
installedLayer.setLayerEventMask(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK
| AWTEvent.MOUSE_WHEEL_EVENT_MASK | AWTEvent.KEY_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK);
}
/**
* Process the mouse events and map the mouse coordinates inverting the internal affine transformation.
* It consume the original event, calculates the mapped mouse coordinates and find the real target of the mouse event.
* It than create a new event with the correct informations in it and redispatch it to the target event
*
* #param originalEvent the event to be dispatched
* #param layer the layer this LayerUI is set to
*/
private void redispatchMouseEvent(MouseEvent originalEvent, JLayer<? extends JComponent> layer) {
if (layer.getView() != null) {
if (originalEvent.getComponent() != layer.getGlassPane()) {
originalEvent.consume();
}
MouseEvent newEvent = null;
Point realPoint = calculateTargetPoint(layer, originalEvent);
Component realTarget = getTarget(layer, realPoint);
// Component realTarget =
// SwingUtilities.getDeepestComponentAt(layer.getView(),
// realPoint.x, realPoint.y);
if (realTarget != null) {
//System.out.println(realTarget.getClass().getName());
realTarget = getListeningComponent(originalEvent, realTarget);
}
switch (originalEvent.getID()) {
case MouseEvent.MOUSE_PRESSED:
newEvent = transformMouseEvent(layer, originalEvent, realTarget, realPoint);
if (newEvent != null) {
lastPressedTarget = newEvent.getComponent();
}
break;
case MouseEvent.MOUSE_RELEASED:
newEvent = transformMouseEvent(layer, originalEvent, lastPressedTarget, realPoint);
lastPressedTarget = null;
break;
case MouseEvent.MOUSE_CLICKED:
newEvent = transformMouseEvent(layer, originalEvent, realTarget, realPoint);
lastPressedTarget = null;
break;
case MouseEvent.MOUSE_MOVED:
newEvent = transformMouseEvent(layer, originalEvent, realTarget, realPoint);
generateEnterExitEvents(layer, originalEvent, realTarget, realPoint);
break;
case MouseEvent.MOUSE_ENTERED:
generateEnterExitEvents(layer, originalEvent, realTarget, realPoint);
break;
case MouseEvent.MOUSE_EXITED:
generateEnterExitEvents(layer, originalEvent, realTarget, realPoint);
break;
case MouseEvent.MOUSE_DRAGGED:
newEvent = transformMouseEvent(layer, originalEvent, lastPressedTarget, realPoint);
generateEnterExitEvents(layer, originalEvent, realTarget, realPoint);
break;
case (MouseEvent.MOUSE_WHEEL):
// redispatchMouseWheelEvent((MouseWheelEvent) originalEvent,
// realTarget, realPoint);
newEvent = transformMouseWheelEvent(layer, (MouseWheelEvent) originalEvent, realTarget, realPoint);
break;/**/
}
dispatchMouseEvent(newEvent);
}
}
/**
* Apply the inverse transformation to {#code point}
*
* #param layer the layer this LayerUI is set to
* #param point the starting point
* #return the transformed point
*/
private Point transformPoint(JLayer<? extends JComponent> layer, Point point) {
if (transform != null) {
try {
transform.inverseTransform(point, point);
} catch (NoninvertibleTransformException e) {
e.printStackTrace();
}
}
return point;
}
/**
* Find the deepest component in the AWT hierarchy
*
* #param layer the layer to which this UI is installed
* #param targetPoint the point in layer's coordinates
* #return the component in the specified point
*/
private Component getTarget(JLayer<? extends JComponent> layer, Point targetPoint) {
Component view = layer.getView();
if (view == null) {
return null;
} else {
Point viewPoint = SwingUtilities.convertPoint(layer, targetPoint, view);
return SwingUtilities.getDeepestComponentAt(view, viewPoint.x, viewPoint.y);
}
}
/**
* Convert the {#code mouseEvent}'s coordinates to the {#code layer}'s space
* #param layer the layer this LayerUI is set to
* #param mouseEvent the original mouse event
* #return the {#code mouseEvent}'s point transformed to the {#code layer}'s coordinate space
*/
private Point calculateTargetPoint(JLayer<? extends JComponent> layer,
MouseEvent mouseEvent) {
Point point = mouseEvent.getPoint();
//SwingUtilities.convertPointToScreen(point, mouseEvent.getComponent());
//SwingUtilities.convertPointFromScreen(point, layer);
point = SwingUtilities.convertPoint(mouseEvent.getComponent(), point, layer);
return transformPoint(layer, point);
}
private MouseEvent transformMouseEvent(JLayer<? extends JComponent> layer, MouseEvent mouseEvent, Component target, Point realPoint) {
return transformMouseEvent( layer, mouseEvent, target, realPoint, mouseEvent.getID());
}
/**
* Create the new event to being dispatched
*/
private MouseEvent transformMouseEvent(JLayer<? extends JComponent> layer, MouseEvent mouseEvent, Component target, Point targetPoint, int id) {
if (target == null) {
return null;
} else {
Point newPoint = SwingUtilities.convertPoint(layer, targetPoint, target);
return new MouseEvent(target, //
id, //
mouseEvent.getWhen(), //
mouseEvent.getModifiers(), //
newPoint.x, //
newPoint.y, //
mouseEvent.getClickCount(), //
mouseEvent.isPopupTrigger(), //
mouseEvent.getButton());
}
}
/**
* Create the new mouse wheel event to being dispached
*/
private MouseWheelEvent transformMouseWheelEvent( JLayer<? extends JComponent> layer, MouseWheelEvent mouseWheelEvent, Component target,
Point targetPoint) {
if (target == null) {
return null;
} else {
Point newPoint = SwingUtilities.convertPoint(layer, targetPoint, target);
return new MouseWheelEvent(target, //
mouseWheelEvent.getID(), //
mouseWheelEvent.getWhen(), //
mouseWheelEvent.getModifiers(), //
newPoint.x, //
newPoint.y, //
mouseWheelEvent.getClickCount(), //
mouseWheelEvent.isPopupTrigger(), //
mouseWheelEvent.getScrollType(), //
mouseWheelEvent.getScrollAmount(), //
mouseWheelEvent.getWheelRotation() //
);
}
}
/**
* dispatch the {#code mouseEvent}
* #param mouseEvent the event to be dispatched
*/
private void dispatchMouseEvent(MouseEvent mouseEvent) {
if (mouseEvent != null) {
Component target = mouseEvent.getComponent();
target.dispatchEvent(mouseEvent);
}
}
/**
* Get the listening component associated to the {#code component}'s {#code event}
*/
private Component getListeningComponent(MouseEvent event, Component component) {
switch (event.getID()) {
case (MouseEvent.MOUSE_CLICKED):
case (MouseEvent.MOUSE_ENTERED):
case (MouseEvent.MOUSE_EXITED):
case (MouseEvent.MOUSE_PRESSED):
case (MouseEvent.MOUSE_RELEASED):
return getMouseListeningComponent(component);
case (MouseEvent.MOUSE_DRAGGED):
case (MouseEvent.MOUSE_MOVED):
return getMouseMotionListeningComponent(component);
case (MouseEvent.MOUSE_WHEEL):
return getMouseWheelListeningComponent(component);
}
return null;
}
/**
* Cycles through the {#code component}'s parents to find the {#link Component} with associated {#link MouseListener}
*/
private Component getMouseListeningComponent(Component component) {
if (component.getMouseListeners().length > 0) {
return component;
} else {
Container parent = component.getParent();
if (parent != null) {
return getMouseListeningComponent(parent);
} else {
return null;
}
}
}
/**
* Cycles through the {#code component}'s parents to find the {#link Component} with associated {#link MouseMotionListener}
*/
private Component getMouseMotionListeningComponent(Component component) {
/*
* Mouse motion events may result in MOUSE_ENTERED and MOUSE_EXITED.
*
* Therefore, components with MouseListeners registered should be
* returned as well.
*/
if (component.getMouseMotionListeners().length > 0 || component.getMouseListeners().length > 0) {
return component;
} else {
Container parent = component.getParent();
if (parent != null) {
return getMouseMotionListeningComponent(parent);
} else {
return null;
}
}
}
/**
* Cycles through the {#code component}'s parents to find the {#link Component} with associated {#link MouseWheelListener}
*/
private Component getMouseWheelListeningComponent(Component component) {
if (component.getMouseWheelListeners().length > 0) {
return component;
} else {
Container parent = component.getParent();
if (parent != null) {
return getMouseWheelListeningComponent(parent);
} else {
return null;
}
}
}
/**
* Generate a {#code MOUSE_ENTERED} and {#code MOUSE_EXITED} event when the target component is changed
*/
private void generateEnterExitEvents( JLayer<? extends JComponent> layer,MouseEvent originalEvent, Component newTarget, Point realPoint) {
if (lastEnteredTarget != newTarget) {
dispatchMouseEvent(
transformMouseEvent(layer, originalEvent, lastEnteredTarget, realPoint, MouseEvent.MOUSE_EXITED));
lastEnteredTarget = newTarget;
//System.out.println("Last " + lastEnteredTarget.getClass().getName());
dispatchMouseEvent(
transformMouseEvent(layer, originalEvent, lastEnteredTarget, realPoint, MouseEvent.MOUSE_ENTERED));
}
}
}
ZoomPanel is a JPanel which include a JLayer with a TransformUI in it. The JLayer contains a JPanel with a SpringLayout in which the constraints are updated with the scale factor to layout correctly the components (JXLayer had it's own LayoutManager but in JLayer you can't set it)
package jzoom.transform;
import ...
public class ZoomPanel extends JPanel {
private static final long serialVersionUID = 1L;
private AffineTransform transform;
private TransformUI layerUI;
private JLayer<JComponent> layer;
private SpringLayout layout;
private JPanel springPanel;
private Container view = null;
public ZoomPanel() {
this(null);
}
public ZoomPanel(Container view) {
setLayout(new BorderLayout());
this.view = view;
transform = new AffineTransform();
layout = new SpringLayout();
springPanel = new JPanel(layout);
if (view != null) {
updateConstraints();
springPanel.add(view);
}
layerUI = new TransformUI();
layerUI.setTransform(transform);
layer = new JLayer<JComponent>(springPanel, layerUI);
super.add(layer);
}
private void updateConstraints() {
Spring width = layout.getConstraint(SpringLayout.WIDTH, springPanel);
Spring height = layout.getConstraint(SpringLayout.HEIGHT, springPanel);
SpringLayout.Constraints constraints = layout.getConstraints(view);
constraints.setX(Spring.constant(0));
constraints.setY(Spring.constant(0));
constraints.setWidth(Spring.scale(width, (float) (1 / transform.getScaleX())));
constraints.setHeight(Spring.scale(height, (float) (1 / transform.getScaleX())));
}
public void setView(Container view) {
if (this.view != null) {
throw new IllegalStateException(
this.getClass().getName() + " cannot be shared between multiple containers");
}
if (view != null) {
this.view = view;
updateConstraints();
springPanel.add(view);
} else {
throw new IllegalArgumentException("Can't set a null view");
}
}
public double getScale() {
return transform.getScaleX();
}
public void zoomIn() {
setScale(transform.getScaleX() + 0.1);
}
public void zoomOut() {
setScale(transform.getScaleX() - 0.1);
}
public void setScale(double scale) {
if (!(scale < 1)) {
transform.setToIdentity();
transform.scale(scale, scale);
updateConstraints();
springPanel.updateUI();
}
}
protected Component addToView(Component comp, Object constraints, int index) {
if (view != null) {
view.add(comp, constraints, index);
return comp;
}
if (comp instanceof Container) {
setView((Container) comp);
return view;
}
throw new IllegalStateException("You need to add or set a Container view before adding Components");
}
#Override
public Component add(Component comp) {
// TODO Auto-generated method stub
return addToView(comp, null, this.getComponentCount());
}
#Override
public Component add(Component comp, int index) {
// TODO Auto-generated method stub
return addToView(comp, null, index);
}
#Override
public void add(Component comp, Object constraints) {
// TODO Auto-generated method stub
addToView(comp, constraints, this.getComponentCount());
}
#Override
public void add(Component comp, Object constraints, int index) {
// TODO Auto-generated method stub
addToView(comp, constraints, index);
}
private void inspectView(Container view) {
PrintStream ps = null;
try {
ps = new PrintStream("C:\\Users\\andrea.maracci\\Documents\\sicraReflectionTemp.txt");
inspectView(view, 0, ps);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (ps != null) {
ps.close();
}
}
}
private static void inspectView(Component component, Integer level, PrintStream ps) {
for (Integer i = 0; i < level; i++) {
ps.print("\t");
}
ps.print(level + ")");
ps.println("Inspecting " + component.getClass().getName());
int accessibleCount = 0;
if (component.getAccessibleContext() != null) {
accessibleCount = component.getAccessibleContext().getAccessibleChildrenCount();
if (accessibleCount > 0) {
ps.println("*********************************************ACCESSIBLE CONTEXT*********************************************");
for (int i = 0; i < accessibleCount; i++) {
ps.println(i + ") " + component.getAccessibleContext().getAccessibleChild(i).getClass().getName());
}
ps.println("************************************************************************************************************");
}
}
if (component instanceof JComponent) {
JComponent jComponent = ((JComponent)component);
if (jComponent.getComponentCount() > 0) {
Component[] children = jComponent.getComponents();
for (Component child : children) {
inspectView(child, ++level, ps);
}
}
}
--level;
}
}
And here is an horrible test program
package jzoom.test;
import ...
public class TestFinal {
public static void main(String[] args) {
new TestFinal();
}
public TestFinal() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (UnsupportedLookAndFeelException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setMinimumSize(new Dimension(400,500));
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private JLayer<JComponent> layer;
private TransformUI layerUI;
private JPanel content;
private AffineTransform transform = new AffineTransform();
private ZoomPanel zoomPanel;
public TestPane() {
content = new JPanel(new GridBagLayout());
// content = new JPanel(new XYLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridy = 0;
gbc.weighty = 0;
gbc.weightx = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel label = new JLabel("Hello");
JTextField field = new JTextField("World", 20);
content.add(label, gbc);
gbc.weightx = 1;
content.add(field, gbc);
// content.add(label, new XYConstraints(50, 20, 50, 22));
// content.add(field, new XYConstraints(100, 20, 200, 22));
gbc.gridy++;
gbc.gridwidth = 2;
final JSlider slider = new JSlider(100, 200);
slider.setValue(100);
slider.addChangeListener(new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
int value = slider.getValue();
double scale = value / 100d;
zoomPanel.setScale(scale);
}
});
content.add(slider, gbc);
// content.add(slider, new XYConstraints(75, 50, 200, 50));
gbc.gridy++;
gbc.gridwidth = 2;
gbc.weighty = 1;
gbc.fill = GridBagConstraints.BOTH;
JTextArea textArea = new JTextArea();
textArea.setEditable(true);
textArea.setText(
"pollofritto\npalma\npalmipedone\ncaccoletta\namammata\na\nasd\nasdgfag\nasdafa\nasdfasf\nadsfasdf\nadfadsf\nadsfdasf\nasdfdas\npollofritto\npalma\npalmipedone\ncaccoletta\namammata\na\nasd\nasdgfag\nasdafa\nasdfasf\nadsfasdf\nadfadsf\nadsfdasf\nasdfdas");
// textArea.setPreferredSize(new Dimensions());
JScrollPane scrollPane = new JScrollPane(textArea);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setPreferredSize(new Dimension(200, 75));
content.add(scrollPane, gbc);
gbc.gridy++;
gbc.gridwidth = 2;
gbc.weighty = 0;
gbc.weightx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
String[] petStrings = { "Bird", "Cat", "Dog", "Rabbit", "Pig" };
JComboBox petList = new JComboBox(petStrings);
content.add(petList, gbc);
JButton zoomIn = new JButton("Zoom In");
// zoomIn.addMouseListener(new ZoomMouseListener());
zoomIn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// System.out.println("FIRST");
// layerUI.zoomIn();
//double zoom = transform.getScaleX();
//transform.setToIdentity();
//transform.scale(zoom + 0.1, zoom + 0.1);
zoomPanel.zoomIn();
// jLayer.repaint();
}
});
JButton zoomOut = new JButton("Zoom Out");
zoomOut.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
zoomPanel.zoomOut();
//double zoom = transform.getScaleX();
//transform.setToIdentity();
//transform.scale(zoom - 0.1, zoom - 0.1);
// jLayer.repaint();
}
});
gbc.gridy++;
gbc.gridx = 0;
gbc.gridwidth = 0;
gbc.anchor = GridBagConstraints.LINE_END;
// content.add(zoomOut, new XYConstraints(50, 120, 100, 25));
// content.add(zoomIn, new XYConstraints(170, 120, 100, 25));
JPanel button = new JPanel();
button.setLayout(new BoxLayout(button, BoxLayout.LINE_AXIS));
button.add(zoomOut);
button.add(zoomIn);
gbc.fill = GridBagConstraints.NONE;
content.add(button, gbc);
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
zoomPanel = new ZoomPanel();
zoomPanel.setView(content);
add(zoomPanel);
}
}
}
Related
I have an application where I want to use a JSlider for user input, representing a percentage of an absolute value.
Based on other variables, the minimum/maximum percentage value will be limited. I don't want to change the scale (min/max) of the JSlider itself (I want to preserve the full-range percentage scale). I created a ChangeListener that determines if the range needs to be limited, and when so, I call the JSlider.setValue() method with the appropriate (limited min/max) value. However, the JSlider itself does not reflect the value I've set. If I query the value with JSlider.getValue() it returns what I would expect (the value I set).
Is there a way to force the JSlider to only allow dragging the actual slider (thumb, knob) to a certain point, then stopping?
Update:
After spending too much time on this, I've decided that the root problem is that I'm misusing/abusing the JSlider in an unintended use model. Restricting the range of movement of the thumb to be less than the BoundedRangeModel range requires modification of the BasicSliderUI class. While the original solution proposed by aterai below does work, it requires overriding a SliderUI implementation class, impacting the portability and consistency of a plaf. So, either I have to find a different UI element or modify the JSlider BoundedRangeModel limits based on the other interdependent variable values. The downside of the latter is that the same thumb position will represent a different value based on the value of other user-editable parameters.
You might be able to override the createTrackListener(...) method of MetalSliderUI to prevent that dragging.
Edit
Another option is to use a JLayer(untested code, may take some customization to work for other LookAndFeel):
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.*;
import javax.swing.plaf.basic.BasicSliderUI;
// import javax.swing.plaf.metal.MetalSliderUI;
// import javax.swing.plaf.synth.SynthSliderUI;
// import com.sun.java.swing.plaf.windows.WindowsSliderUI;
public class DragLimitedSliderTest {
private static int MAXI = 80;
private JComponent makeUI() {
JSlider slider1 = makeSlider();
JSlider slider2 = makeSlider();
slider2.setUI(new BasicSliderUI(slider2) {
//slider2.setUI(new WindowsSliderUI(slider2) {
//slider2.setUI(new MetalSliderUI() {
//slider2.setUI(new SynthSliderUI(slider2) {
#Override protected TrackListener createTrackListener(JSlider slider) {
return new TrackListener() {
#Override public void mouseDragged(MouseEvent e) {
//case HORIZONTAL:
int halfThumbWidth = thumbRect.width / 2;
int thumbLeft = e.getX() - offset;
int maxPos = xPositionForValue(MAXI) - halfThumbWidth;
if (thumbLeft > maxPos) {
int x = maxPos + offset;
MouseEvent me = new MouseEvent(
e.getComponent(), e.getID(), e.getWhen(), e.getModifiers(),
x, e.getY(),
e.getXOnScreen(), e.getYOnScreen(),
e.getClickCount(), e.isPopupTrigger(), e.getButton());
e.consume();
super.mouseDragged(me);
} else {
super.mouseDragged(e);
}
}
};
}
});
JSlider slider3 = makeSlider();
JPanel p = new JPanel(new GridLayout(3, 1));
p.add(slider1);
p.add(slider2);
p.add(new JLayer<JSlider>(slider3, new DisableInputLayerUI()));
return p;
}
private static JSlider makeSlider() {
JSlider slider = new JSlider(0, 100, 40) {
#Override public void setValue(int n) {
super.setValue(n);
}
};
slider.setMajorTickSpacing(10);
slider.setPaintTicks(true);
slider.setPaintLabels(true);
Dictionary dictionary = slider.getLabelTable();
if (dictionary != null) {
Enumeration elements = dictionary.elements();
while (elements.hasMoreElements()) {
JLabel label = (JLabel) elements.nextElement();
int v = Integer.parseInt(label.getText());
if (v > MAXI) {
label.setForeground(Color.RED);
}
}
}
slider.getModel().addChangeListener(new ChangeListener() {
#Override public void stateChanged(ChangeEvent e) {
BoundedRangeModel m = (BoundedRangeModel) e.getSource();
if (m.getValue() > MAXI) {
m.setValue(MAXI);
}
}
});
return slider;
}
public static void main(String... args) {
EventQueue.invokeLater(() -> {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
// for (UIManager.LookAndFeelInfo laf: UIManager.getInstalledLookAndFeels()) {
// if ("Nimbus".equals(laf.getName())) {
// UIManager.setLookAndFeel(laf.getClassName());
// }
// }
} catch (Exception e) {
e.printStackTrace();
}
JFrame f = new JFrame();
f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
f.getContentPane().add(new DragLimitedSliderTest().makeUI());
f.setSize(320, 240);
f.setLocationRelativeTo(null);
f.setVisible(true);
});
}
}
class DisableInputLayerUI extends LayerUI<JSlider> {
#Override public void installUI(JComponent c) {
super.installUI(c);
if (c instanceof JLayer) {
JLayer jlayer = (JLayer) c;
jlayer.setLayerEventMask(
AWTEvent.MOUSE_EVENT_MASK |
AWTEvent.MOUSE_MOTION_EVENT_MASK);
}
}
#Override public void uninstallUI(JComponent c) {
if (c instanceof JLayer) {
JLayer jlayer = (JLayer) c;
jlayer.setLayerEventMask(0);
}
super.uninstallUI(c);
}
private Rectangle thumbRect = new Rectangle(11, 19); //magic number
private Rectangle focusRect = new Rectangle();
private Rectangle contentRect = new Rectangle();
private Rectangle trackRect = new Rectangle();
private int offset;
protected int xPositionForValue(JSlider slider, int value) {
int min = slider.getMinimum();
int max = slider.getMaximum();
int trackLength = trackRect.width;
double valueRange = (double) max - (double) min;
double pixelsPerValue = (double) trackLength / valueRange;
int trackLeft = trackRect.x;
int trackRight = trackRect.x + (trackRect.width - 1);
int xPosition;
xPosition = trackLeft;
xPosition += Math.round(pixelsPerValue * ((double) value - min));
xPosition = Math.max(trackLeft, xPosition);
xPosition = Math.min(trackRight, xPosition);
return xPosition;
}
protected int getHeightOfTallestLabel(JSlider slider) {
Dictionary dictionary = slider.getLabelTable();
int tallest = 0;
if (dictionary != null) {
Enumeration keys = dictionary.keys();
while (keys.hasMoreElements()) {
JComponent label = (JComponent) dictionary.get(keys.nextElement());
tallest = Math.max(label.getPreferredSize().height, tallest);
}
}
return tallest;
}
#Override protected void processMouseEvent(MouseEvent e, JLayer<? extends JSlider> l) {
JSlider slider = l.getView();
if (e.getID() == MouseEvent.MOUSE_PRESSED) {
//case HORIZONTAL:
//recalculateIfInsetsChanged()
Insets insetCache = slider.getInsets();
Insets focusInsets = UIManager.getInsets("Slider.focusInsets");
if (focusInsets == null) {
focusInsets = new Insets(2, 2, 2, 2); //magic number
}
//calculateFocusRect()
focusRect.x = insetCache.left;
focusRect.y = insetCache.top;
focusRect.width = slider.getWidth() - (insetCache.left + insetCache.right);
focusRect.height = slider.getHeight() - (insetCache.top + insetCache.bottom);
//calculateContentRect()
contentRect.x = focusRect.x + focusInsets.left;
contentRect.y = focusRect.y + focusInsets.top;
contentRect.width = focusRect.width - (focusInsets.left + focusInsets.right);
contentRect.height = focusRect.height - (focusInsets.top + focusInsets.bottom);
//calculateThumbSize()
Icon ti = UIManager.getIcon("Slider.horizontalThumbIcon");
if (ti != null) {
thumbRect.width = ti.getIconWidth();
thumbRect.height = ti.getIconHeight();
}
//calculateTrackBuffer()
int trackBuffer = 9; //magic number, Windows: 9, Metal: 10 ...
//calculateTrackRect()
int centerSpacing = thumbRect.height;
if (slider.getPaintTicks()) centerSpacing += 8; //magic number getTickLength();
if (slider.getPaintLabels()) centerSpacing += getHeightOfTallestLabel(slider);
trackRect.x = contentRect.x + trackBuffer;
trackRect.y = contentRect.y + (contentRect.height - centerSpacing - 1) / 2;
trackRect.width = contentRect.width - (trackBuffer * 2);
trackRect.height = thumbRect.height;
//calculateThumbLocation()
int valuePosition = xPositionForValue(slider, slider.getValue());
thumbRect.x = valuePosition - (thumbRect.width / 2);
thumbRect.y = trackRect.y;
offset = e.getX() - thumbRect.x;
}
}
#Override protected void processMouseMotionEvent(MouseEvent e, JLayer<? extends JSlider> l) {
if (e.getID() == MouseEvent.MOUSE_DRAGGED) {
JSlider slider = l.getView();
//case HORIZONTAL:
int halfThumbWidth = thumbRect.width / 2;
int thumbLeft = e.getX() - offset;
int maxPos = xPositionForValue(slider, 80) - halfThumbWidth;
if (thumbLeft > maxPos) {
e.consume();
SliderUI ui = slider.getUI();
if (ui instanceof BasicSliderUI) {
((BasicSliderUI) ui).setThumbLocation(maxPos, thumbRect.y);
}
slider.getModel().setValue(80);
}
}
}
}
It seems there's really no way to do what I wanted in a way that will be consistent across different Look and Feel implementations. Tracing through the notifications and listeners of the JSlider, BasicSliderUI, and BoundedRangeModel it turns out that there is no way to force a recalculation of the thumb position through public methods and the base listener implementation specifically prevents this from happening when dragging or terminating the drag (mouse release). See update to original question for more.
Recently I have created two very simple, yet very useful components called AttributesEditableView and AttributesEditor. AttributesEditableView is designed to just show arbitrary number of attributes, while the AttributesEditor makes it possible to edit attributes.
So, say we have three attributes that can take values 'Y', 'N' and '?' (where by '?' we mean "unspecified"). The view looks something like [[Y][N][?]]:
In the editor, user can use arrow-keys to jump left-right between attributes, and by pressing the SPACE key toggle the value of particular (currently selected) attribute.
(I guess those of you who are familiar with JTable and its peculiarities already understand when am I getting to right now).
It all works well in a form, but when I want to make a TableCellEditor out of this AttributesEditor, I get stuck, because JTable either "eats" all events, or there is something that I am doing wrong so my AttributesEditor simply does not get events... :(
My logic is the following: how come DefaultCellEditor when the editor component is JTextField gets those events (left-key or right-key presses for example), while my cell-editor does not?
I know I am not giving all the classes here, but I believe the two classes below should give those who have already encountered this problem enough information to be able to tell me how to make this cell editor work as expected?
UPDATE: I managed to fix the problem by refactoring the AttributesEditor class. Now it uses key-bindings instead of the KeyListener and everything works as expected. Lesson learned...
You can see the AttributesEditableView, AttributesEditor and AttributesCellEditor in action on the following screen shot (I also have the renderer, but could not bother to put it on, it looks like AttributesEditableView anyway):
In the screenshot above, first attribute may have 'Y', 'N' and '?' values, second one 'A', 'B', 'C', and 'D', and the third attribute may have 'T' and 'F' as values.
Here are the classes:
AttributesCellEditor
/**
* Authors: Dejan Lekic , http://dejan.lekic.org
* License: MIT
*/
public class AttributesCellEditor extends AbstractCellEditor
implements TableCellEditor, TreeCellEditor {
private AttributesEditor attributesEditor;
private AttributesColumnModel attrColModel;
private AttributesModel model;
private int clickCountToStart = 2;
DefaultCellEditor dfe;
public AttributesCellEditor(AttributesColumnModel argModel) {
super();
attrColModel = argModel;
model = new AttributesModel(attrColModel.getAllowedValues());
model.setDefaultValues(attrColModel.getDefaultValues());
attributesEditor = new AttributesEditor(model);
attributesEditor.setBorder(new BevelBorder(BevelBorder.LOWERED));
}
// ::::: CellEditor method implementations :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
#Override
public Object getCellEditorValue() {
System.out.println("getCellEditorValue()");
// before we return the value, we should also modify the attributes column model and set the
// selected attribute index to the index of what we last edited.
attrColModel.setSelectedAttributeIndex(model.getSelectedAttributeIndex());
return model.getAttributes();
}
// ::::: TableCellEditor method implementations ::::::::::::::::::::::::::::::::::::::::::::::::::::::::
#Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row,
int column) {
String val = (value == null) ? "" : value.toString();
model.setAttributes(val);
model.setSelectedAttributeIndex(attrColModel.getSelectedAttributeIndex());
attributesEditor.setModel(model);
return attributesEditor;
}
// ::::: TreeCellEditor method implementations ::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// TODO: implement these
#Override
public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected,
boolean expanded, boolean leaf, int row) {
//To change body of generated methods, choose Tools | Templates.
throw new UnsupportedOperationException("Not supported yet.");
}
// ::::: AbstractCellEditor method overrides ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
#Override
public boolean isCellEditable(EventObject e) {
if (e instanceof MouseEvent) {
return ((MouseEvent) e).getClickCount() >= clickCountToStart;
}
return true;
}
/**
* {#inheritDoc}
* #return
*/
#Override
public boolean stopCellEditing() {
boolean ret = super.stopCellEditing();
return ret;
}
} // AttributesCellEditor class
AttributesEditor
/**
* Authors: Dejan Lekic , http://dejan.lekic.org
* License: MIT
*/
public class AttributesEditor
extends AttributesEditableView
implements PropertyChangeListener, FocusListener, KeyListener {
public AttributesEditor() {
super();
editorComponent = true;
setFocusable(true);
setBackground(Color.white);
// Let's copy the border from the TextField
Border b = (Border) UIManager.getLookAndFeelDefaults().get("TextField.border");
setBorder(b);
// Let's copy the insets from the TextField
Insets insets = (Insets) UIManager.getLookAndFeelDefaults().get("TextField.margin");
getInsets().set(insets.top, insets.left, insets.bottom, insets.right);
addKeyListener(this);
// listeners...
addFocusListener(this);
}
public AttributesEditor(AttributesModel argModel) {
this();
setOpaque(true);
setFocusable(true);
setBackground(Color.white);
repaint();
setModel(argModel);
}
// :::: PropertyChangeListener method implementations :::::::::::::::::::::::::::::::::::::::::::::::::
#Override
public void propertyChange(PropertyChangeEvent evt) {
String str = (String) evt.getNewValue();
System.out.println("CALL: AttributesEditor.propertyChange() : " + str);
updateView();
}
// :::: FocusListener method implementations ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
#Override
public void focusGained(FocusEvent e) {
int sel = model.getSelectedAttributeIndex();
if (sel < 0) {
model.setSelectedAttributeIndex(0);
}
sel = model.getSelectedAttributeIndex();
labels[sel].setBackground(model.getSelectedBackgroundColor());
}
#Override
public void focusLost(FocusEvent e) {
model.setSelectedAttributeIndex(-1);
updateView();
}
// :::: KeyListener method implementations ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
#Override
public void keyTyped(KeyEvent e) {
// Weird thing is, arrow keys do not trigger keyTyped() to be called,
// so we have to use keyPressed here, or keyReleased
}
/**
* Weird thing is, arrow keys do not trigger keyTyped() to be called, so we have to use keyPressed/keyReleased here.
* #param e
*/
#Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
switch (key) {
case KeyEvent.VK_UP:
model.previous(model.getSelectedAttributeIndex());
break;
case KeyEvent.VK_DOWN:
case KeyEvent.VK_SPACE:
model.next(model.getSelectedAttributeIndex());
break;
case KeyEvent.VK_LEFT:
model.previousAttribute();
updateView();
break;
case KeyEvent.VK_RIGHT:
model.nextAttribute();
updateView();
break;
default:
int idx = model.getSelectedAttributeIndex();
char chr = Character.toUpperCase(e.getKeyChar());
if (model.isValid(idx, chr)) {
model.setValue(idx, chr);
}
} // switch
} // keyPressed() method
#Override
public void keyReleased(KeyEvent e) {
// nothing
}
} // AttributesEditor class
AttributesEditableView
/**
* Authors: Dejan Lekic , http://dejan.lekic.org
* License: MIT
*/
public class AttributesEditableView
extends JComponent
implements PropertyChangeListener, MouseListener {
protected AttributesModel model;
protected JLabel[] labels;
protected boolean editorComponent;
protected boolean inTable;
public AttributesEditableView() {
super();
FlowLayout fl = new FlowLayout(FlowLayout.LEFT);
fl.setVgap(0);
setLayout(fl);
editorComponent = false;
inTable = false;
}
public AttributesEditableView(AttributesModel argModel) {
this();
setModel(argModel);
}
// :::: JComponent method overrides :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
/**
* I had to do this because it is a common problem with JComponent subclasses.
* More about it: http://docs.oracle.com/javase/tutorial/uiswing/painting/problems.html
*
* #param g
*/
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(getForeground());
}
// :::: PropertyChangeListener mthod implementations ::::::::::::::::::::::::::::::::::::::::::::::::::
#Override
public void propertyChange(PropertyChangeEvent evt) {
String str = (String) evt.getNewValue();
updateView();
}
// :::: <Interface> method implementations ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
#Override
public void mouseClicked(MouseEvent e) {
// labels have names in form of "attributelbl-3", we need to extract the ID from the name
String num = e.getComponent().getName().split("-")[1];
int idx = Integer.parseInt(num);
if (editorComponent) {
model.setSelectedAttributeIndex(idx);
}
model.next(idx);
} // mouseClicked() method
#Override
public void mousePressed(MouseEvent e) {
// do nothing
}
#Override
public void mouseReleased(MouseEvent e) {
// do nothing
}
#Override
public void mouseEntered(MouseEvent e) {
// labels have names in form of "attributelbl-3", we need to extract the ID from the name
String num = e.getComponent().getName().split("-")[1];
int idx = Integer.parseInt(num);
model.setSelectedAttributeIndex(idx);
updateView();
}
#Override
public void mouseExited(MouseEvent e) {
// labels have names in form of "attributelbl-3", we need to extract the ID from the name
String num = e.getComponent().getName().split("-")[1];
int idx = Integer.parseInt(num);
if (!editorComponent) {
model.setSelectedAttributeIndex(-1);
}
updateView();
}
/**
* Use this method to forcefully highlight specific attribute.
* #param argIndex
*/
public final void highlight(int argIndex) {
for (int i = 0; i < model.getNumberOfAttributes(); i++) {
if (i == argIndex) {
labels[i].setBackground(model.getSelectedBackgroundColor());
} else {
labels[i].setBackground(model.getBackgroundColor());
}
} // for
} // highlight() method
/**
* Extremely important method. Here we set the model, and generate JLabel objects for the attributes.
* We also set the values, and initialise the listeners.
* #param argModel
*/
public final void setModel(AttributesModel argModel) {
if (model != null) {
model.removePropertyChangeListener(this);
}
labels = null;
removeAll();
labels = new JLabel[argModel.getNumberOfAttributes()];
int w = 0;
int h = 0;
for (int i = 0; i < argModel.getNumberOfAttributes(); i++) {
String txt = "" + argModel.getValue(i);
System.out.println(txt);
JLabel lbl = new JLabel(txt);
labels[i] = lbl;
lbl.setName("attributelbl-" + i); // very important, because we will use the name to get the idx
lbl.setHorizontalAlignment(SwingConstants.CENTER);
lbl.setOpaque(true);
lbl.setBackground(argModel.getBackgroundColor());
if (isInTable()) {
lbl.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, Color.GRAY));
}
int nh = lbl.getPreferredSize().height;
int nw = nh; // make the width to be equal to the height
lbl.setPreferredSize(new Dimension(nw, nh));
lbl.addMouseListener(this);
h = Math.max(h, lbl.getPreferredSize().height);
w = w + lbl.getPreferredSize().width;
add(lbl);
} // for
Dimension ps = new Dimension(w, h);
model = argModel;
model.addPropertyChangeListener(this);
} // setModel() method
public final AttributesModel getModel() {
return model;
}
public boolean isInTable() {
return inTable;
}
public void setInTable(boolean inTable) {
this.inTable = inTable;
}
/**
* Updates the view
*/
protected void updateView() {
String attrs = model.getAttributes();
for (int i = 0; i < model.getNumberOfAttributes(); i++) {
labels[i].setText("" + model.getValue(i));
if (model.getSelectedAttributeIndex() == i) {
labels[i].setBackground(model.getSelectedBackgroundColor());
} else {
labels[i].setBackground(model.getBackgroundColor());
}
}
}
} // AttributesEditableView class
I have a JPanel where I have components which are moveable.
My problem is that if I use a action in the panel the locations of components in the whole panel are changing theire position to top mid for just a frame than changing back theire positions. I debugged it and know that its coming from validate(). If I use validate() manually it happens without actions, too.
So here is the code for the components that are used in the panel:
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.SystemColor;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import org.pentaho.reporting.engine.classic.core.DataFactory;
import org.pentaho.reporting.engine.classic.core.designtime.DataSourcePlugin;
import com.inform_ac.utils.misc.gui.resizer.ComponentResizer;
/**
* AButton is a JAVA-Swing component with integrated functional buttons.
*/
public class AButton extends JPanel {
/**
* SerialVersionUID: 111111111L
*/
protected static final long serialVersionUID = 111111111L;
/**
* Standard font for displaying text.
*/
protected Font standardFont = new Font("Dialog", Font.PLAIN, 12);
/**
* Defining functional JButtons to delete and edit a AButton.
*/
protected ADeleteButton delBtn = new ADeleteButton(this);
protected JButton editBtn = new JButton();
protected String[] editOptionNames = { "Connect", "Deconnect", "Edit" };
protected JPopupMenu popupmenu = new JPopupMenu();
protected Dimension minDimension = new Dimension(120,120);
protected Point location = new Point();
protected DataFactory dataFactory;
protected DataSourcePlugin dataSourcePlugin;
/**
* Mode: 0 - moving
* 1 - connecting
*/
protected int mode = 0;
/**
* Defining the label for displaying OK or error images.
*/
protected JLabel lblIcon = new JLabel();
protected final JLabel lblInfo1 = new JLabel();
protected final JLabel lblInfo2 = new JLabel();
protected final JLabel lblInfo3 = new JLabel();
public final JButton connectBtn_right = new JButton();
public final JButton connectBtn_left = new JButton();
protected AButton parent;
protected ArrayList<AButton> children = new ArrayList<AButton>();
/**
* Identifier
*/
protected int id = 0;
/**
* Constructor with given start coordinates.
*
* #param X
* - coordinate
* #param Y
* - coordinate
*/
public AButton(int X, int Y, int index) {
this.id = index;
location = new Point(X,Y);
setBounds(location.x, location.y, minDimension.width, minDimension.height);
init();
}
/**
* Private method to initialize main components.
*/
private void init() {
initPnl();
initBtns();
setVisible(true);
setFocusable(true);
}
/**
* Private method to initialize the main panel.
*/
private void initPnl() {
setBackground(UIManager.getColor("InternalFrame.activeTitleBackground"));
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
setPreferredSize(minDimension);
setBorder(UIManager.getBorder("CheckBox.border"));
setFont(standardFont);
setLayout(null);
}
/**
* Private method to initialize functional {#linkplain JButton}.
*/
private void initBtns() {
initEditBtn();
initDelBtn();
initIconPnl();
initConnectorBtns();
}
/**
* Private method to initialize the delete button. Method have to refresh
* the size of this AButton to set the button on the top right corner of
* this AButton.
*/
private void initDelBtn() {
delBtn.setBounds(getWidth() - 18 - 2, 2, 18, 18);
delBtn.setFont(standardFont);
delBtn.setBackground(null);
delBtn.setBorderPainted(false);
delBtn.setBorder(null);
delBtn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
delBtn.setPreferredSize(new Dimension(16, 16));
delBtn.setMinimumSize(new Dimension(12, 12));
delBtn.setMaximumSize(new Dimension(20, 20));
delBtn.setIcon(new ImageIcon(AButton.class
.getResource("/javax/swing/plaf/metal/icons/ocean/close.gif")));
add(delBtn);
}
/**
* Private method to initialize the edit button.
*/
private void initEditBtn() {
initPopupmenu();
initMouseListener();
editBtn.setBounds(2,2,21,21);
editBtn.setFont(standardFont);
editBtn.setBorder(null);
editBtn.setBorderPainted(false);
editBtn.setBackground(UIManager
.getColor("InternalFrame.activeTitleGradient"));
editBtn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
editBtn.setAlignmentX(Component.CENTER_ALIGNMENT);
editBtn.setPreferredSize(new Dimension(21, 21));
editBtn.setMinimumSize(new Dimension(18, 18));
editBtn.setMaximumSize(new Dimension(25, 25));
editBtn.setIcon(new ImageIcon("C:\\Users\\akaradag\\Pictures\\JavaIcon\\icon_bearbeiten.gif"));
add(editBtn);
}
protected void initPopupmenu(){
for(int i = 0; i < editOptionNames.length; i++) {
popupmenu.add(new AbstractAction(editOptionNames[i]) {
private static final long serialVersionUID = 5550466652812249477L;
#Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("Connect")) {
if (mode == 1) {
mode = 0;
showConnectors();
} else {
mode = 1;
showConnectors();
}
} else if (e.getActionCommand().equals("Deconnect")) {
resetConnections();
}
else if(e.getActionCommand().equals("Edit")) {
}
}
});
}
}
protected void initMouseListener()
{
editBtn.addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
popupmenu.show(e.getComponent(), e.getX(), e.getY());
}
});
}
/**
* Private method to display or not display the connector buttons
*/
public void showConnectors() {
boolean connect = false;
if (mode == 1) {
connect = true;
}
connectBtn_left.setVisible(connect);
connectBtn_right.setVisible(connect);
}
/**
* Private method to initialize the connector buttons
*/
private void initConnectorBtns() {
connectBtn_right.setCursor(Cursor
.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
connectBtn_right.setVisible(false);
connectBtn_right.setPreferredSize(new Dimension(15, 15));
connectBtn_right.setMinimumSize(new Dimension(12, 12));
connectBtn_right.setMaximumSize(new Dimension(15, 15));
connectBtn_right.setFont(new Font("Dialog", Font.PLAIN, 12));
connectBtn_right.setBorderPainted(false);
connectBtn_right.setBorder(null);
connectBtn_right.setBackground(SystemColor.activeCaption);
connectBtn_right
.setBounds(getWidth() - 16, getHeight() / 2 - 5, 15, 15);
add(connectBtn_right);
connectBtn_left.setCursor(Cursor
.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
connectBtn_left.setVisible(false);
connectBtn_left.setPreferredSize(new Dimension(25, 25));
connectBtn_left.setMinimumSize(new Dimension(12, 12));
connectBtn_left.setMaximumSize(new Dimension(15, 15));
connectBtn_left.setFont(new Font("Dialog", Font.PLAIN, 12));
connectBtn_left.setBorderPainted(false);
connectBtn_left.setBorder(null);
connectBtn_left.setBackground(SystemColor.activeCaption);
connectBtn_left.setBounds(2, getHeight() / 2 - 5, 15, 15);
add(connectBtn_left);
}
/**
* Private method to initialize the {#linkplain JLabel} for displaying
* informations.
*/
private void initIconPnl() {
lblIcon.setHorizontalTextPosition(SwingConstants.CENTER);
lblIcon.setHorizontalAlignment(SwingConstants.CENTER);
lblIcon.setAlignmentX(Component.CENTER_ALIGNMENT);
lblIcon.setIcon(new ImageIcon(AButton.class
.getResource("/javax/swing/plaf/metal/icons/ocean/error.png")));
lblIcon.setBorder(null);
lblIcon.setFont(new Font("Dialog", Font.PLAIN, 12));
lblIcon.setBounds(getWidth() / 4, 3, getWidth() / 2,
getHeight() / 4 + 2);
lblIcon.setLayout(null);
add(lblIcon);
lblInfo1.setFont(new Font("Tahoma", Font.BOLD, getHeight() / 10));
lblInfo1.setForeground(SystemColor.desktop);
lblInfo1.setBounds(22, getHeight() / 2 - 5, getWidth() - 42,
getHeight() / 8);
add(lblInfo1);
lblInfo2.setFont(new Font("Tahoma", Font.BOLD, getHeight() / 10));
lblInfo2.setForeground(Color.BLACK);
lblInfo2.setBounds(10, getHeight() / 2 - 5 + getHeight() / 8 + 5,
getWidth() - 20, getHeight() / 8);
add(lblInfo2);
lblInfo3.setFont(new Font("Tahoma", Font.BOLD, getHeight() / 10));
lblInfo3.setForeground(Color.BLACK);
lblInfo3.setBounds(10, getHeight() / 2 - 5 + 2 * (getHeight() / 8 + 5),
getWidth() - 20, getHeight() / 8);
add(lblInfo3);
}
public String getLblInfo(int index) {
if (index == 1) {
return lblInfo1.getText();
} else if (index == 2) {
return lblInfo2.getText();
} else {
return lblInfo3.getText();
}
}
public void setLblInfo(String text, int index) {
if (index == 1) {
lblInfo1.setText(text);
} else if (index == 2) {
lblInfo2.setText(text);
} else {
lblInfo3.setText(text);
}
}
public Point getLocation() {
return new Point(getX(), getY());
}
public Point getInputLocation() {
return connectBtn_left.getLocation();
}
/**
* Methode um die Location des Objektes zu ändern und dies auch zu repainten.
* Dient dazu damit der Fehler das wenn ein Objekt gelöscht wird die restlichen in die Mitte
* wandern.
*/
public void setLocation(Point p){
location = p;
}
public Point getOutputLocation() {
return connectBtn_right.getLocation();
}
public int getMode() {
return mode;
}
public void setMode(int mode) {
this.mode = mode;
showConnectors();
}
public JButton getDelBtn() {
return delBtn;
}
public int getIndex() {
return id;
}
public void setIndex(int index) {
this.id = index;
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
this.setLocation(location.x, location.y);
if (lblInfo1.getText().equals("Join")) {
if (children.size() == 2) {
lblIcon.setIcon(new ImageIcon(
"C:\\Users\\akaradag\\Pictures\\JavaIcon\\ok-icon.png"));
} else if (children.size() > 2) {
lblIcon.setIcon(new ImageIcon(
AButton.class
.getResource("/javax/swing/plaf/metal/icons/ocean/warning.png")));
} else {
lblIcon.setIcon(new ImageIcon(
AButton.class
.getResource("/javax/swing/plaf/metal/icons/ocean/error.png")));
}
} else if (lblInfo1.getText().equals("JDBC")
|| lblInfo1.getText().equals("File")) {
if (parent != null) {
lblIcon.setIcon(new ImageIcon(
"C:\\Users\\akaradag\\Pictures\\JavaIcon\\ok-icon.png"));
} else {
lblIcon.setIcon(new ImageIcon(
AButton.class
.getResource("/javax/swing/plaf/metal/icons/ocean/error.png")));
}
}
}
public void setOutput(AButton out) {
parent = out;
}
public AButton getOutput() {
return parent;
}
public void addInput(AButton input) {
if (!contains(input)) {
this.children.add(input);
}
}
private boolean contains(AButton in) {
for (int i = 0; i < this.children.size(); i++) {
if (this.children.get(i).getIndex() == in.getIndex()) {
return true;
}
}
return false;
}
public ArrayList<AButton> getInput() {
return this.children;
}
public void removeFromInput(AButton remove) {
for (int i = 0; i < this.children.size(); i++) {
if (this.children.get(i) != null) {
if (this.children.get(i).getIndex() == remove.getIndex()) {
this.children.remove(i);
}
}
}
}
public void resetConnections() {
if (parent != null) {
ArrayList<AButton> in = parent.getInput();
for (int i = 0; i < in.size(); i++) {
if (in.get(i).getIndex() == id) {
in.remove(i);
}
}
parent = null;
}
for (int i = 0; i < this.children.size(); i++) {
this.children.get(i).setOutput(null);
this.children.get(i).repaint();
}
this.children = new ArrayList<AButton>();
// if(connectionsDeletedNotify != null)
// connectionsDeletedNotify.actionPerformed(new ActionEvent(this, 0, "Deconnect"));
}
/**
* Setter for the DataFactory
* #param df
*/
public void setDataFactory(DataFactory df)
{
this.dataFactory = df;
}
/**
* Getter for the DataFactory
* #return
*/
public DataFactory getDataFactory()
{
return this.dataFactory;
}
public DataSourcePlugin getDataSourcePlugin() {
return dataSourcePlugin;
}
public void setDataSourcePlugin(DataSourcePlugin dataSourcePlugin) {
this.dataSourcePlugin = dataSourcePlugin;
}
}
Here is the code of the main panel
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import org.pentaho.reporting.designer.core.settings.WorkspaceSettings;
import org.pentaho.reporting.engine.classic.core.DataFactory;
import org.pentaho.reporting.engine.classic.core.MasterReport;
import org.pentaho.reporting.engine.classic.core.designtime.DataSourcePlugin;
import org.pentaho.reporting.engine.classic.core.designtime.DefaultDataFactoryChangeRecorder;
import org.pentaho.reporting.engine.classic.core.metadata.DataFactoryMetaData;
import org.pentaho.reporting.engine.classic.core.metadata.DataFactoryRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.inform_ac.reporting.datasource.dataintegrator.frontend.DataintegratorDesignTimeContext;
import com.inform_ac.reporting.datasource.dataintegrator.frontend.component.AButton;
import com.inform_ac.reporting.datasource.dataintegrator.frontend.component.AConfirmDialog;
import com.inform_ac.reporting.datasource.dataintegrator.frontend.component.ADeleteButton;
public class DataintegratorMainPanel extends JPanel {
DataintegratorDesignTimeContext context;
JPopupMenu popmen = new JPopupMenu();
JMenuItem menu1 = new JMenuItem("Add new Datasource:");
JMenuItem menu2 = new JMenuItem("Join");
Dimension dim = new Dimension();
Point holdingPoint , point1, point2;;
/**
* ArrayList für die visuellen Datasources
*/
ArrayList<AButton> abuttonList = new ArrayList<AButton>();
/**
* Nummerierungen der abuttons
*/
int index = 0;
/**
* The <code>Logger</code> of this instance
*/
protected final static Logger LOGGER = LoggerFactory.getLogger(DataintegratorQueryPanel.class);
private static final long serialVersionUID = 4705352682546516889L;
public DataintegratorMainPanel() {
initPopupMenu();
initMouseListener();
}
protected void initMouseListener()
{
this.addMouseListener(new MouseAdapter() {
#Override
public void mouseReleased(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON3){
popmen.show(e.getComponent(), e.getX(), e.getY());
}
}
});
}
/**
* Überschrift bei Rechtsklick
* #param header
* #return JComponent
*/
protected JComponent createHeader(String header) {
JLabel label = new JLabel(header);
label.setFont(label.getFont().deriveFont(Font.BOLD,14));
label.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
return label;
}
protected void giveMouseListenerTo(AButton button) {
button.addMouseMotionListener(new MouseAdapter() {
#Override
public void mouseDragged(MouseEvent me) {
super.mouseDragged(me);
AButton tmp = (AButton) me.getSource();
//Mode Moving
if(tmp.getMode()==0){
Point point = getMousePosition();
//Über den Rand raus
if(point != null) {
point.x = point.x - holdingPoint.x;
point.y = point.y - holdingPoint.y;
if(point.x >= 0 && point.y >= 0) {
tmp.setLocation(point);
}
//LOGGER.info(""+point);
}
}
else if (tmp.getMode()==1) {
point2 = getMousePosition();
}
repaint();
}
});
button.addMouseListener(new MouseAdapter() {
#Override
public void mousePressed(MouseEvent me) {
super.mousePressed(me);
AButton tmp = (AButton) me.getSource();
if (tmp.getMode() == 0) {
holdingPoint = tmp.getMousePosition();
}
else if (tmp.getMode() == 1) {
Point tmp_point = tmp.getLocation();
point1 = new Point(tmp.connectBtn_right.getLocation().x+tmp_point.x,tmp.connectBtn_right.getLocation().y+tmp_point.y);
LOGGER.info("point1: "+point1);
}
}
#Override
public void mouseReleased(MouseEvent me) {
super.mouseReleased(me);
try {
AButton destination = (AButton) getComponentAt(getMousePosition());
AButton source = (AButton) me.getSource();
if (destination != null && source.getMode() == 1) {
// Hier muss der Baum verkettet werden
if (destination != source) {
destination.addInput(source);
if (source.getOutput() == null) {
source.setOutput(destination);
} else {
AButton tmp = source.getOutput();
tmp.removeFromInput(source);
source.setOutput(destination);
}
source.setMode(0);
destination.setMode(0);
}
}
} catch (ClassCastException e) {
point2 = null;
}
point1 = null;
point2 = null;
repaint();
}
});
repaint();
}
protected void initPopupMenu() {
popmen.add(createHeader("Neue Datasource"));
popmen.addSeparator();
context = new DataintegratorDesignTimeContext(new MasterReport());
final DataFactoryMetaData[] datas = DataFactoryRegistry.getInstance()
.getAll();
for (final DataFactoryMetaData data : datas) {
// Some of the DataFactories are not needed
if (data.isHidden()) {
continue;
} else if (!WorkspaceSettings.getInstance().isShowExpertItems()
&& data.isExpert()) {
continue;
} else if (!WorkspaceSettings.getInstance().isShowDeprecatedItems()
&& data.isDeprecated()) {
continue;
} else if (!WorkspaceSettings.getInstance()
.isExperimentalFeaturesVisible() && data.isExperimental()) {
continue;
} else if (!data.isEditorAvailable()) {
continue;
}
if (data.getDisplayName(getLocale()).equals("Dataintegrator")) {
continue;
}
JMenuItem item = new JMenuItem(new AbstractAction(
data.getDisplayName(getLocale())) {
private static final long serialVersionUID = 7700562297221703939L;
#Override
public void actionPerformed(ActionEvent arg0) {
Point mousePos = getMousePosition();
final AButton tmp = new AButton(mousePos.x,mousePos.y,index++);
// Action listener hier und nicht in die for schleife
tmp.getDelBtn().addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
ADeleteButton tmp_1 = (ADeleteButton) arg0
.getSource();
AButton source = tmp_1.getAbutton();
AConfirmDialog dialog = new AConfirmDialog();
int opt = dialog.getState();
if (opt == 1) {
for (int i = 0; i < abuttonList.size(); i++) {
if (source.getIndex() != abuttonList.get(i)
.getIndex()) {
if (abuttonList.get(i).getOutput() != null
&& abuttonList.get(i).getOutput()
.getIndex() == source
.getIndex()) {
abuttonList.get(i).setOutput(null);
}
abuttonList.get(i).removeFromInput(source);
}
}
// seperat erst die referenzen löschen dann das
// objekt aus der liste
for (int i = 0; i < abuttonList.size(); i++) {
if (source.getIndex() == abuttonList.get(i)
.getIndex()) {
abuttonList.remove(i);
source.setVisible(false);
source.setEnabled(false);
}
}
}
}
});
LOGGER.info("--> data: " + data);
final DataSourcePlugin editor = data.createEditor();
LOGGER.info("--> editor: " + editor);
final DefaultDataFactoryChangeRecorder recorder = new DefaultDataFactoryChangeRecorder();
final DataFactory dataFactory = editor.performEdit(context,
null, null, recorder);
LOGGER.info("--> datafactory: " + dataFactory);
// Falls die Datafactory initialisiert werden konnte
if (dataFactory != null) {
tmp.setDataSourcePlugin(editor);
tmp.setDataFactory(dataFactory);
add(tmp);
//LOGGER.info("New datafact"+mousePos);
abuttonList.add(tmp);
//Moving listener
giveMouseListenerTo(tmp);
validate();
}
}
});
popmen.add(item);
}
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setStroke(new BasicStroke(1.5f));
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
Line2D tmp_line;
for (int i = 0; i < abuttonList.size(); i++) {
if (abuttonList.get(i).getOutput() != null) {
g2d.setStroke(new BasicStroke(1.5f));
int x1 = abuttonList.get(i).getLocation().x
+ abuttonList.get(i).connectBtn_right.getLocation().x;
int y1 = abuttonList.get(i).getLocation().y
+ abuttonList.get(i).connectBtn_right.getLocation().y;
int x2 = abuttonList.get(i).getOutput().getLocation().x
+ abuttonList.get(i).getOutput().connectBtn_left
.getLocation().x - 4;
int y2 = abuttonList.get(i).getOutput().getLocation().y
+ abuttonList.get(i).getOutput().connectBtn_left
.getLocation().y;
tmp_line = new Line2D.Double(x1, y1, x2, y2);
double m = (y2 - y1) / (double) (x2 - x1);
//logger.info("m: "+m);
int vz = x2-x1;
//LOGGER.info("vz: "+vz);
if(vz<0)
{
vz = 20;
}
else if (vz>0){
vz = -20;
}
if(m<4 && m>-4) {
Point p = new Point(x2 -20, (int) (y1 + m * (x2 - x1 - 20)));
//logger.info(p.toString());
Line2D tmp2_line = new Line2D.Double(x2, y2, x2 + vz,
p.y + 3);
//logger.info("x2: "+x2+", y2: "+y2+", p.y:"+p.y);
Line2D tmp3_line = new Line2D.Double(x2, y2, x2 + vz,
p.y - 3);
//logger.info("x2: "+x2+", y2: "+y2+", p.y:"+p.y);
g2d.setPaint(Color.BLACK);
g2d.draw(tmp2_line);
g2d.draw(tmp3_line);
}
g2d.draw(tmp_line);
}
}
if (point1 != null && point2 != null) {
Line2D line2d = new Line2D.Double(point1, point2);
g2d.setPaint(Color.RED);
g2d.setStroke(new BasicStroke(1.5f));// set stroke size
g2d.draw(line2d);
}
}
}
So the error happens if I click on the editBtn und click for example on Connect. It repaints the whole panel and the abuttons in the panel are located in the middle top of the panel vertically for some frames and it goes back to the locations that are saved in the object itselft.
Also I can reproduce the error by using validate().
I dont understand what is the problem here. Why its changing the locations of the components in the main panel after a validation?
And sorry if the code is not SSCCE but I couldn't get it work without the pentaho libraries...
Ok I fixed it.
The problem was the default layoutmanager of the JPanel.
After I changed it to setLayout(null); the problem disapears.
I suggest the use of a layout manager, in any respect it is preferable; it may take you some time at first, but is worthy. For simple use cases, built-in ones, like BorderLayout, GridBagLayout may suffice. But, for more control, try MigLayout: complete, elegant, and precise. (http://www.miglayout.com/)
If you ask why, the JavaDoc has it:
void java.awt.Container.validate()
Validates this container and all of its subcomponents.
Validating a container means laying out its subcomponents. Layout-related changes, such as setting the bounds of a component, or adding a component to the container, invalidate the container automatically. Note that the ancestors of the container may be invalidated also (see Component.invalidate for details.) Therefore, to restore the validity of the hierarchy, the validate() method should be invoked on the top-most invalid container of the hierarchy.
Validating the container may be a quite time-consuming operation. For performance reasons a developer may postpone the validation of the hierarchy till a set of layout-related operations completes, e.g. after adding all the children to the container.
If this Container is not valid, this method invokes the validateTree method and marks this Container as valid. Otherwise, no action is performed.
Overrides: validate() in Component
The code I'm referencing is proprietary and requires a multicast server, so I can't post an SSCCE snippet. I understand this may preclude any helpful insight which would elicit viable responses...
I'm compiling with Java 7 u 9.
I'm currently using a JTable in a GUI app that listens to multicast data, and displays it as it arrives. When scrolling the table or resizing columns, the app responds excruciatingly slow.
I thought I structured my code appropriately.
I used a wrapper class, and in it's main() function, it creates an instance of itself, which processes command line arguments, creates listeners, creates the JFrame and calls the class that returns a JTable. This is all done outside of the event dispatch thread.
Then, in the next line, I used the invokeLater() method to create a thread that handles all of the GUI rendering. It creates a JScrollPane, adds the JTable to the scroll pane, sets the scroll bars, sets the viewport, sets the scroll mode, and adds the JScrollPane to a JFrame . This is all handled within the event dispatch thread.
The rows typically populate fairly quick, with the occasional screen freeze (some of the rows contain 30 or more lines), but the responsiveness seems acceptable. However, when scrolling or resizing columns, the response is very slow.
All of the examples I've seen, including the SwingX SwingLabs demo all refer to an initial dataset that is loaded up front. I need an example of using a JTable with streaming data.
Can anyone point me to such an example/demo?
This is my main() snippet...
public static void main(String args[])
{
final JXTrapTableGUI ttg = new JXTrapTableGUI(args);
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
ttg.createAndShowGUI();
}
});
}
PS. I want to thank each and everyone who responded. I've been pulled off this project until March 11th, but I will review all responses on that date.
I do not think that JTable works well with streaming data at all. All of the optimization techniques you mentioned, like keeping processing off of the Event Dispatch Thread, are irrlevant if the TableModel does not contain a real list but instead some connection to a data stream.
Without seeing how you attempted to handle this, it's hard to know exactly why it's slow. But here is how I would make it repsonsive:
Create a ListModel that stores a List - not a reference to a stream, just a plain List.
Have another Thread capture Multicast Data from the stream, lets call it DataStreamCollector.
Then launch a Thread that runs on a timer (javax.swing.Timer) that checks with the DataStreamCollector and updates the ListModel as needed.
My design here is assuming that UI responsiveness is more important than 100% synchronization with the data stream. Adjusting the timers should let you trade off having an up-to-date table with having a responsive UI.
Somewhere lost in Oracles clean_up to the trash (old Suns tutorials),
This project was called ChristmastTree, is about JTable & Performance,
Standard Java code before crazy & messy SwingWorker invoked from black hole called Executor
import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.basic.*;
import javax.swing.table.*;
/**
* CTTable extends JTable doing the following: <ul> <li>The UI is forced to be
* CTTableUI so that a customer CellRendererPane can be installed.
* <li>getCellRenderer is overriden to return the TableCellRenderer passed into
* the constructor. <li>tableChanged is overriden to pass the call to super only
* if the cell is visible. </ul>
*/
public class CTTable extends JTable {
private static final long serialVersionUID = 1L;
private CTTableCellRenderer renderer;
public CTTable(CTTableCellRenderer renderer) {
super();
this.renderer = renderer;
renderer.setFont(getFont());
}
#Override
public void updateUI() {
super.updateUI();
//Force the UI to be an instanceof CTTableUI. This approach will not work
//if you need to support more than one look and feel in your application.
setUI(new CTTableUI());
}
#Override
public void setFont(Font font) {
super.setFont(font);
if (renderer != null) {
renderer.setFont(font);
}
}
#Override
public TableCellRenderer getCellRenderer(int row, int column) {
return renderer;
}
#Override
public void tableChanged(TableModelEvent e) {
if (e instanceof VisibleTableModelEvent && !((VisibleTableModelEvent) e).isVisible(this)) {
return;// Do nothing if this cell isn't visible.
}
super.tableChanged(e);
}
private static class CTTableUI extends BasicTableUI {
#Override
public void installUI(JComponent c) {
super.installUI(c);// Overriden to install our own CellRendererPane
c.remove(rendererPane);
rendererPane = new CTCellRendererPane();
c.add(rendererPane);
}
}
/**
* CTCellRendererPane overrides paintComponent to NOT clone the Graphics
* passed in and NOT validate the Component passed in. This will NOT work if
* the painting code of the Component clobbers the graphics (scales it,
* installs a Paint on it...) and will NOT work if the Component needs to be
* validated before being painted.
*/
private static class CTCellRendererPane extends CellRendererPane {
private static final long serialVersionUID = 1L;
private Rectangle tmpRect = new Rectangle();
#Override
public void repaint() {
// We can safely ignore this because we shouldn't be visible
}
#Override
public void repaint(int x, int y, int width, int height) {
}
#Override
public void paintComponent(Graphics g, Component c, Container p, int x,
int y, int w, int h, boolean shouldValidate) {
if (c == null) {
if (p != null) {
Color oldColor = g.getColor();
g.setColor(p.getBackground());
g.fillRect(x, y, w, h);
g.setColor(oldColor);
}
return;
}
if (c.getParent() != this) {
this.add(c);
}
c.setBounds(x, y, w, h);
// As we are only interested in using a JLabel as the renderer,
//which does nothing in validate we can override this to do nothing,
//if you need to support components that can do layout, this will
//need to be commented out, or conditionally validate.
shouldValidate = false;
if (shouldValidate) {
c.validate();
}
boolean wasDoubleBuffered = false;
JComponent jc = (c instanceof JComponent) ? (JComponent) c : null;
if (jc != null && jc.isDoubleBuffered()) {
wasDoubleBuffered = true;
jc.setDoubleBuffered(false);
}//Don't create a new Graphics, reset the clip and translate the origin.
Rectangle clip = g.getClipBounds(tmpRect);
g.clipRect(x, y, w, h);
g.translate(x, y);
c.paint(g);
g.translate(-x, -y);
g.setClip(clip.x, clip.y, clip.width, clip.height);
if (wasDoubleBuffered) {
jc.setDoubleBuffered(true);
}
c.setBounds(-w, -h, 0, 0);
}
}
}
.
import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.table.*;
/**
* A custom TableCellRenderer that overrides a handful of methods: <ul>
* <li>isOpaque and setBackground are overridden to avoid filling the
* background, if possible. <li>firePropertyChange is overridden to do nothing.
* If you need to support HTML text in the renderer than this should NOT be
* overridden. <li>paint is overridden to forward the call directly to the UI,
* avoiding the creation of a Graphics. This will NOT work if you need the
* renderer to contain other childre or the Graphics is clobbered as part of
* painting the UI. </ul>
*/
public class CTTableCellRenderer extends DefaultTableCellRenderer {
private static final long serialVersionUID = 1L;
private Color background;
private Color foreground;
private Color editableForeground;
private Color editableBackground;
private Border focusBorder;
public CTTableCellRenderer() {
focusBorder = UIManager.getBorder("Table.focusCellHighlightBorder");
editableForeground = UIManager.getColor("Table.focusCellForeground");
editableBackground = UIManager.getColor("Table.focusCellBackground");
}
#Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
boolean negative = (value != null && ((Integer) value).intValue() < 0);
// Reset the background based on the sign of the value.
if (isSelected) {
setForeground(table.getSelectionForeground());
setBackground(table.getSelectionBackground());
} else {
setForeground(table.getForeground());
if (!negative) {
setBackground(null);
} else {
setBackground(Color.red);
}
}//NOTICE that we do NOT set the font here, because CTTable knows about
//us, it will set the font as appropriate.
if (hasFocus) {
setBorder(focusBorder);
if (table.isCellEditable(row, column)) {
setForeground(editableForeground);
setBackground(editableBackground);
}
} else {
setBorder(noFocusBorder);
}
setValue(value);
return this;
}
#Override
protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
// As long as you don't have any HTML text, this override is ok.
}
#Override// This override is only appropriate if this will never contain
// any children AND the Graphics is not clobbered during painting.
public void paint(Graphics g) {
ui.update(g, this);
}
#Override
public void setBackground(Color c) {
this.background = c;
}
#Override
public Color getBackground() {
return background;
}
#Override
public void setForeground(Color c) {
this.foreground = c;
}
#Override
public Color getForeground() {
return foreground;
}
#Override
public boolean isOpaque() {
return (background != null);
}
#Override // This is generally ok for non-Composite components (like Labels)
public void invalidate() {
}
#Override // Can be ignored, we don't exist in the containment hierarchy.
public void repaint() {
}
}
.
import javax.swing.table.*;
import java.util.*;
/**
* CTTableModel, a TableModel, models a set of Datas as the rows. The data is
* stored in a List of Lists. As the changes come in against a particular Data
* object we also contain a map from Data to row. This can obviously be made
* faster by pushing the row to the Data, but this may not be feasable in
* applications of this sort.
*/
public class CTTableModel extends AbstractTableModel {
private static final long serialVersionUID = 1L;
/**
* Maps from Data to an integer id giving the row of the data.
*/
private Map rowMap;
/**
* Number of columns to display.
*/
private int columns;
/**
* A List of Lists.
*/
private java.util.List rowData;
/**
* If true, batch cell updates using sharedModelEvent.
*/
private boolean batchChange;
/**
* Shared model event.
*/
private VisibleTableModelEvent sharedModelEvent;
public CTTableModel(int columns) {
this.columns = columns;
// Notice how they are not synchronized, we do NOT access this class
// from another thread, and therefore do not have to worry about
// synchronization.
rowData = new ArrayList();
rowMap = new HashMap();
}
public void addRow(Data rowID) {
int row = rowData.size();
rowMap.put(rowID, new Integer(row));
ArrayList colData = new ArrayList();
for (int counter = 0; counter < columns; counter++) {
colData.add(null);
}
rowData.add(colData);
fireTableRowsInserted(row, row);
}
/**
* Toggles batch updates. When true and model changes are notified using a
* VisibleTableModelEvent.
*
* #param batch
*/
public void setBatchUpdates(boolean batch) {
this.batchChange = batch;
if (sharedModelEvent == null) {
sharedModelEvent = new VisibleTableModelEvent(this);
}
sharedModelEvent.reset();
}
public boolean getBatchUpdates() {
return batchChange;
}
/**
* Sets the display value for a particular Data item at a particular cell.
* If notify is true listeners are notified, otherwise no listeners are
* notified.
*
* #param rowID
* #param col
* #param data
* #param notify
*/
public void set(Data rowID, int col, Object data, boolean notify) {
int row = ((Integer) rowMap.get(rowID)).intValue();
((java.util.List) rowData.get(row)).set(col, data);
if (notify) {
if (batchChange) {
sharedModelEvent.set(row, col);
fireTableChanged(sharedModelEvent);
} else {
fireTableCellUpdated(row, col);
}
}
}
#Override
public int getRowCount() {
return rowData.size();
}
#Override
public int getColumnCount() {
return columns;
}
#Override
public Object getValueAt(int rowIndex, int columnIndex) {
return ((java.util.List) rowData.get(rowIndex)).get(columnIndex);
}
}
.
/**
* Unique ID for the data.
*/
public class Data {
/**
* This is overriden to remind developers they should have an intelligent
* equals and hashCode. You do not need to override either of them, but if
* you override one you need to override the other. Additionaly, because
* they are used extensively to map the data that has changed to the table,
* equals and hashCode MUST be fast, cache data if you need to!
*
* #param x
*/
#Override
public boolean equals(Object x) {
return (this == x);
}
/**
* This is overriden to remind developers they should have an intelligent
* equals and hashCode. You do not need to override either of them, but if
* you override one you need to override the other. Additionaly, because
* they are used extensively to map the data that has changed to the table,
* equals and hashCode MUST be fast, cache data if you need to!
*/
#Override
public int hashCode() {
return super.hashCode();
}
}
.
import java.util.ArrayList;
/**
* DataChange is used to associate a Data Object with a column identifier that
* has changed. To avoid loads of garbage per update DataChanges are cached and
* reused.
*/
public class DataChange {
private static ArrayList sharedDataChanges = new ArrayList();
private Data data;
private int col;
private int hashCode;
/**
* Obtains a DataChange for the specified Data and column.
*
* #param data
* #param col
* #return
*/
public static DataChange getDataChange(Data data, int col) {
synchronized (sharedDataChanges) {
int size = sharedDataChanges.size();
if (size > 0) {
DataChange change = (DataChange) sharedDataChanges.remove(size - 1);
change.data = data;
change.col = col;
return change;
}
}
return new DataChange(data, col);
}
/**
* Indicates the DataChange is no longer needed and can be reused.
*
* #param change
*/
public static void releaseDataChange(DataChange change) {
synchronized (sharedDataChanges) {
sharedDataChanges.add(change);
}
}
DataChange(Data data, int col) {
this.data = data;
this.col = col;
hashCode = (data.hashCode() | col);
}
public Data getData() {
return data;
}
public int getColumn() {
return col;
}
#Override
public int hashCode() {
return hashCode;
}
public boolean equals(DataChange dc) {
if (dc == this) {
return true;
}
DataChange o = (DataChange) dc;
return (o.data == data && o.col == col);
}
}
.
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
/**
* The Main controller, responsible for wiring everything together. Pressing
* return in any of the fields will trigger recreation of everything.
*/
public class Main implements ActionListener {
// properties: columnCount, rowCount, updateSleepTime, eqSleepTime,
// threshold, generateSleep, generatorBatchCount
private static final Insets EMPTY_INSETS = new Insets(0, 0, 0, 0);
private JTextField columnCount;
private JTextField rowCount;
private JTextField updateSleepTime;
private JTextField eqSleepTime;
private JTextField threshold;
private JTextField generateSleep;
private JTextField generatorBatchCount;
private JFrame frame;
static JLabel totalUpdateTime;
static JLabel notifyTime;
static JLabel paintTime;
static JLabel updateCount;
private JTable table;
private UpdateThread updateThread;
private GeneratorThread generatorThread;
private CTTableModel tableModel;
private static int NUM_COLUMNS = 40;// Initial values for the 7 properties.
private static int NUM_ROWS = 3000;
private static int UPDATE_SLEEP_TIME = 500;
private static int EQ_SLEEP_TIME = 10;
private static int UPDATE_ALL_THRESHOLD = 400000;
private static int GENERATOR_SLEEP_TIME = 40;
private static int BATCH_SIZE = 1000;
Main() {
frame = new JFrame();
frame.getContentPane().setLayout(new GridBagLayout());
columnCount = add("Columns: ", NUM_COLUMNS);
rowCount = add("Rows: ", NUM_ROWS);
updateSleepTime = add("Update Sleep: ", UPDATE_SLEEP_TIME);
eqSleepTime = add("EQ Sleep: ", EQ_SLEEP_TIME);
threshold = add("Update All Threshold: ", UPDATE_ALL_THRESHOLD);
generateSleep = add("Generator Sleep: ", GENERATOR_SLEEP_TIME);
generatorBatchCount = add("Batch Size: ", BATCH_SIZE);
table = new CTTable(new CTTableCellRenderer());
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
JScrollPane sp = new JScrollPane(table);
frame.getContentPane().add(sp, new GridBagConstraints(0, 3, 6, 1, 1, 1,
GridBagConstraints.WEST, GridBagConstraints.BOTH, EMPTY_INSETS, 0, 0));
ChangeListener changeListener = new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
BoundedRangeModel m = (BoundedRangeModel) (e.getSource());
if (updateThread != null) {
updateThread.setUpdatesEnabled(!(m.getValueIsAdjusting()));
}
}
};
sp.getVerticalScrollBar().getModel().addChangeListener(changeListener);
sp.getHorizontalScrollBar().getModel().addChangeListener(changeListener);
totalUpdateTime = new JLabel(" ");
notifyTime = new JLabel(" ");
paintTime = new JLabel(" ");
updateCount = new JLabel(" ");
JPanel statusPanel = new JPanel(new GridBagLayout());
frame.getContentPane().add(statusPanel, new GridBagConstraints(0, 4, 6, 1, 1, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, EMPTY_INSETS, 0, 0));
statusPanel.add(totalUpdateTime, new GridBagConstraints(0, 0, 1, 1, 1, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, EMPTY_INSETS, 0, 0));
statusPanel.add(notifyTime, new GridBagConstraints(1, 0, 1, 1, 1, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, EMPTY_INSETS, 0, 0));
statusPanel.add(paintTime, new GridBagConstraints(2, 0, 1, 1, 1, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, EMPTY_INSETS, 0, 0));
statusPanel.add(updateCount, new GridBagConstraints(3, 0, 1, 1, 1, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, EMPTY_INSETS, 0, 0));
frame.setTitle("Christmas Tree Demo Application");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setBounds(0, 0, 1000, 800);
frame.setVisible(true);
try {
Thread.sleep(5000);
} catch (InterruptedException ie) {
}
reset();
}
#Override
public void actionPerformed(ActionEvent ae) {
reset();
}
private JTextField add(String name, int defaultValue) {
Container parent = frame.getContentPane();
int row = parent.getComponentCount() / 6;
int col = parent.getComponentCount() % 6;
parent.add(new JLabel(name), new GridBagConstraints(col, row, 1, 1, 0, 0,
GridBagConstraints.WEST, 0, EMPTY_INSETS, 0, 0));
JTextField tf = new JTextField(Integer.toString(defaultValue));
tf.addActionListener(this);
parent.add(tf, new GridBagConstraints(col + 1, row, 1, 1, 1, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, EMPTY_INSETS, 0, 0));
return tf;
}
private void reset() {
System.out.println("Columns: " + getInt(columnCount));
System.out.println("Rows: " + getInt(rowCount));
System.out.println("Update Sleep: " + getInt(updateSleepTime));
System.out.println("EQ Sleep: " + getInt(eqSleepTime));
System.out.println("Update All Threshold: " + getInt(threshold));
System.out.println("Generator Sleep: " + getInt(generateSleep));
System.out.println("Batch Size: " + getInt(generatorBatchCount));
if (updateThread != null) {
System.out.println("interrupting!");
updateThread.interrupt();
generatorThread.interrupt();
}
int cols = getInt(columnCount);
tableModel = new CTTableModel(cols);
ArrayList<Data> data = new ArrayList<Data>();
for (int counter = getInt(rowCount) - 1; counter >= 0; counter--) {
Data dataID = new Data();
data.add(dataID);
tableModel.addRow(dataID);
for (int colCounter = 0; colCounter < cols; colCounter++) {
if (colCounter % 2 == 0) {
tableModel.set(dataID, colCounter,
new Integer(counter * 100 + colCounter), false);
} else {
tableModel.set(dataID, colCounter,
new Integer(counter * -100 + colCounter), false);
}
}
}
table.setModel(tableModel);
generatorThread = new GeneratorThread(data, getInt(generateSleep),
getInt(generatorBatchCount), getInt(columnCount));
updateThread = new UpdateThread(generatorThread, tableModel,
getInt(updateSleepTime), getInt(eqSleepTime), getInt(threshold));
generatorThread.start();
updateThread.start();
}
private int getInt(JTextField tf) {
try {
return Integer.parseInt(tf.getText());
} catch (NumberFormatException nfe) {
System.out.println("exception getting int: " + nfe);
}
return 0;
}
public static void main(String[] args) {
Main main = new Main();
}
}
... will be continue
rest of methods
import java.awt.*;
import java.lang.reflect.*;
import java.util.*;
import javax.swing.*;
/**
* Thread responsible for publishing changes to the Model. Sleeps for a defined
* amount of time, waits for no activity in the UI and then users invokeAndWait
* to publish changes.
*/
public class UpdateThread extends Thread {
private int sleepTime;
private int eqSleepTime;
private int threshhold;
private boolean updatesEnabled;
private Runnable publishRunnable;
private Runnable emptyRunnable;
private GeneratorThread generator;
private CTTableModel model;
private Map<?, ?> lastData;
private long notifyTime;
private long paintTime;
private int updateCount;
private boolean done;
public UpdateThread(GeneratorThread generator, CTTableModel model,
int sleepTime, int eqSleepTime, int threshhold) {
super();
setPriority(Thread.MIN_PRIORITY);
this.sleepTime = sleepTime;
this.eqSleepTime = eqSleepTime;
updatesEnabled = true;
this.threshhold = threshhold;
this.generator = generator;
this.model = model;
publishRunnable = new Runnable() {
// Runnable used to publish changes to the event dispatching thread
#Override
public void run() {
publishChangesOnEventDispatchingThread();
}
};
// Empty runnable, used to wait until the event dispatching thread
// has finished processing any pending events.
emptyRunnable = new Runnable() {
#Override
public void run() {
}
};
}
#Override
public void interrupt() {
done = true;
super.interrupt();
}
#Override
public void run() {
while (!isInterrupted() && !done) {
try {
sleep(sleepTime);
publishChanges();
} catch (InterruptedException ie) {
}
}
System.out.println("UpdateThread done");
}
/**
* Publishes changes on the event dispatching thread when the system isn't
* busy. This blocks the caller until the changes have been published.
*/
private void publishChanges() {
synchronized (this) {// Wait until the user isn't scrolling
while (!updatesEnabled) {
try {
wait();
} catch (InterruptedException ie) {
}
}
}
EventQueue queue = Toolkit.getDefaultToolkit().getSystemEventQueue();
// And wait until there are no pending events.
while (queue.peekEvent() != null) {
try {
sleep(eqSleepTime);
} catch (InterruptedException ie) {
}
}
final long start = System.currentTimeMillis();
try {
SwingUtilities.invokeAndWait(publishRunnable);
// publish the changes on the event dispatching thread
} catch (InterruptedException ie) {
} catch (InvocationTargetException ite) {
}
try {
// Wait until the system has completed processing of any events we
// triggered as part of publishing changes.
SwingUtilities.invokeAndWait(emptyRunnable);
} catch (InterruptedException ie) {
} catch (InvocationTargetException ite) {
}
final long end = System.currentTimeMillis();
try {// Update the display
SwingUtilities.invokeAndWait(new Runnable() {
#Override
public void run() {
Main.totalUpdateTime.setText("Total: "
+ Integer.toString((int) (end - start)));
Main.notifyTime.setText("Notify: "
+ Integer.toString((int) notifyTime));
Main.paintTime.setText("Paint: "
+ Integer.toString((int) paintTime));
Main.updateCount.setText("Updated: "
+ Integer.toString((int) updateCount));
}
});
} catch (InterruptedException ie) {
} catch (InvocationTargetException ite) {
}
}
/**
* Does the actual publishing of changes.
*/
private void publishChangesOnEventDispatchingThread() {
long start = System.currentTimeMillis();
model.setBatchUpdates(true);
Map<?, ?> data = generator.getData();
boolean notify = !(data.size() > threshhold ||
(lastData != null && lastData.size() + data.size() > threshhold));
updateCount = data.size();
if (lastData != null) {
updateCount += lastData.size();
}//Reset the data for the last set of changes we did, this forces the cells to change color.
if (lastData != null) {
publishData(lastData, true, notify);
Iterator<?> dataIterator = lastData.keySet().iterator();
while (dataIterator.hasNext()) {
DataChange.releaseDataChange((DataChange) dataIterator.next());
}
lastData.clear();
}
publishData(data, false, notify);// Publish the current set of data.
model.setBatchUpdates(false);
if (!notify) {
model.fireTableDataChanged();
}
lastData = data;
long end = System.currentTimeMillis();
notifyTime = (end - start);
start = System.currentTimeMillis();
RepaintManager.currentManager(null).paintDirtyRegions();
end = System.currentTimeMillis();
paintTime = (end - start);
}
/**
* Publish the passed in set of data.
*/
private void publishData(Map<?, ?> data, boolean negate, boolean notify) {
Iterator<?> dataIterator = data.keySet().iterator();
while (dataIterator.hasNext()) {
DataChange change = (DataChange) dataIterator.next();
Object value = data.get(change);
if (negate) {
value = new Integer(((Integer) value).intValue() * -1);
}
model.set(change.getData(), change.getColumn(), value, notify);
}
}
/**
* If enable is true, we are allowed to publish changes, otherwise we
* aren't.
*
* #param enable
*/
public void setUpdatesEnabled(boolean enable) {
synchronized (this) {
updatesEnabled = enable;
if (updatesEnabled) {
notify();
}
}
}
public boolean getUpdatesEnabled() {
return updatesEnabled;
}
}
.
import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
/**
* VisibleTableModelEvent adds the method isVisible to test if the cell
* identified by the event is visible.
*/
public class VisibleTableModelEvent extends TableModelEvent {
private static final long serialVersionUID = 1L;
private Point tmpPoint;
// This implementation caches the information for one JTable, it is
// certainly possible to cache it for more than one should
// you have this need.
private boolean valid;
private int firstVisRow;
private int lastVisRow;
private int firstVisCol;
private int lastVisCol;
public VisibleTableModelEvent(TableModel source) {
super(source, 0, 0, 0, UPDATE);
tmpPoint = new Point();
}
/**
* Resets the underlying fields of the TableModelEvent. This assumes no ONE
* is going to cache the TableModelEvent.
*
* #param row
* #param col
*/
public void set(int row, int col) {
firstRow = row;
lastRow = row;
column = col;
}
/**
* Invoked to indicate the visible rows/columns need to be recalculated
* again.
*/
public void reset() {
valid = false;
}
public boolean isVisible(JTable table) {
if (!valid) {// Determine the visible region of the table.
Rectangle visRect = table.getVisibleRect();
tmpPoint.x = visRect.x;
tmpPoint.y = visRect.y;
firstVisCol = table.columnAtPoint(tmpPoint);
firstVisRow = table.rowAtPoint(tmpPoint);
tmpPoint.x += visRect.width;
tmpPoint.y += visRect.height;
lastVisCol = table.columnAtPoint(tmpPoint);
if (lastVisCol == -1) {
lastVisCol = table.getColumnCount() - 1;
}
if ((lastVisRow = table.rowAtPoint(tmpPoint)) == -1) {
lastVisRow = table.getRowCount();
}
valid = true;
}
return (firstRow >= firstVisRow && firstRow <= lastVisRow && column
>= firstVisCol && column <= lastVisCol);
}
}
In fact i know how to implement using CTRL+Z (Undo) and CTRL+Y (Redo) with one JTextField. But i have hundreds of Text Components in my Swing application, so is there a way to apply this for all Text Components in my application, so when i click CTRL+Z in any Text Component it would undo the last entry in that Field ?
I have tried to implement it in EventQueue, but it did not work !
To make all your text components "undoable", you can simply create them using your own subclass like:
public class MyTextField extends JTextField {
public MyTextField() {
final UndoManager undoMgr = new UndoManager();
// Add listener for undoable events
getDocument().addUndoableEditListener(new UndoableEditListener() {
public void undoableEditHappened(UndoableEditEvent pEvt) {
undoMgr.addEdit(pEvt.getEdit());
}
});
// Add undo/redo actions
getActionMap().put(UNDO_ACTION, new AbstractAction(UNDO_ACTION) {
public void actionPerformed(ActionEvent pEvt) {
try {
if (undoMgr.canUndo()) {
undoMgr.undo();
}
} catch (CannotUndoException e) {
e.printStackTrace();
}
}
});
getActionMap().put(REDO_ACTION, new AbstractAction(REDO_ACTION) {
public void actionPerformed(ActionEvent pEvt) {
try {
if (undoMgr.canRedo()) {
undoMgr.redo();
}
} catch (CannotRedoException e) {
e.printStackTrace();
}
}
});
// Create keyboard accelerators for undo/redo actions (Ctrl+Z/Ctrl+Y)
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK),
UNDO_ACTION);
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK),
REDO_ACTION);
}
}
Then instead of creating JTextFields, create MyTextFields. The limitation is that you may also want to create another subclass for JTextArea and so on for other JTextComponents. So it is also possible to use an utility method to add Undo/Redo features to any existing JTextCompoent:
public final static String UNDO_ACTION = "Undo";
public final static String REDO_ACTION = "Redo";
public static void makeUndoable(JTextComponent pTextComponent) {
final UndoManager undoMgr = new UndoManager();
// Add listener for undoable events
pTextComponent.getDocument().addUndoableEditListener(new UndoableEditListener() {
public void undoableEditHappened(UndoableEditEvent evt) {
undoMgr.addEdit(evt.getEdit());
}
});
// Add undo/redo actions
pTextComponent.getActionMap().put(UNDO_ACTION, new AbstractAction(UNDO_ACTION) {
public void actionPerformed(ActionEvent evt) {
try {
if (undoMgr.canUndo()) {
undoMgr.undo();
}
} catch (CannotUndoException e) {
e.printStackTrace();
}
}
});
pTextComponent.getActionMap().put(REDO_ACTION, new AbstractAction(REDO_ACTION) {
public void actionPerformed(ActionEvent evt) {
try {
if (undoMgr.canRedo()) {
undoMgr.redo();
}
} catch (CannotRedoException e) {
e.printStackTrace();
}
}
});
// Create keyboard accelerators for undo/redo actions (Ctrl+Z/Ctrl+Y)
pTextComponent.getInputMap().put(
KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK), UNDO_ACTION);
pTextComponent.getInputMap().put(
KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK), REDO_ACTION);
}
The final and absolutely transparent solution would be to implement your own UI class using Lnf features, but you may want to think twice before making all TextComponents undoable, for memory consumptions reasons for example if you often perform huge text modifications to these components...
you can gel list of built_in KeyBindings short_cuts implemented in the API's
notice you have to check or prepare your code for all accesible Look and Feels
you can get built_in KeyBindings short_cuts and replace that as you expecting, System ClipBoard is very often modified,
never tried but on this forum you can foud out rellevant info How to change, replace KeyBindings short_cuts
list of built_in KeyBindings short_cuts
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.table.*;
import javax.swing.filechooser.*;
public class KeyBindings implements ItemListener {
private static final String PACKAGE = "javax.swing.";
private static final String[] COLUMN_NAMES = {"Action", "When Focused", "When In Focused Window", "When Ancestor"};
private static String selectedItem;
private JComponent contentPane;
private JMenuBar menuBar;
private JTable table;
private JComboBox comboBox;
private Hashtable<String, DefaultTableModel> models;
/*
* Constructor
*/
public KeyBindings() {
models = new Hashtable<String, DefaultTableModel>();
contentPane = new JPanel(new BorderLayout());
contentPane.add(buildNorthComponent(), BorderLayout.NORTH);
contentPane.add(buildCenterComponent(), BorderLayout.CENTER);
resetComponents();
}
/*
* The content pane should be added to a high level container
*/
public JComponent getContentPane() {
return contentPane;
}
/*
* A menu can also be added which provides the ability to switch
* between different LAF's.
*/
public JMenuBar getMenuBar() {
if (menuBar == null) {
menuBar = createMenuBar();
}
return menuBar;
}
/*
* This panel is added to the North of the content pane
*/
private JComponent buildNorthComponent() {
comboBox = new JComboBox();
JLabel label = new JLabel("Select Component:");
label.setDisplayedMnemonic('S');
label.setLabelFor(comboBox);
JPanel panel = new JPanel();
panel.setBorder(new EmptyBorder(15, 0, 15, 0));
panel.add(label);
panel.add(comboBox);
return panel;
}
/*
* Check the key name to see if it is the UI property
*/
private String checkForUIKey(String key) {
if (key.endsWith("UI") && key.indexOf('.') == -1) {
String componentName = key.substring(0, key.length() - 2);// Ignore these components
if (componentName.equals("PopupMenuSeparator") || componentName.equals("ToolBarSeparator") || componentName.equals("DesktopIcon")) {
return null;
} else {
return componentName;
}
}
return null;
}
/*
** Build the emtpy table to be added in the Center
*/
private JComponent buildCenterComponent() {
DefaultTableModel model = new DefaultTableModel(COLUMN_NAMES, 0);
table = new JTable(model) {
private static final long serialVersionUID = 1L;
#Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
table.setAutoCreateColumnsFromModel(false);
table.getColumnModel().getColumn(0).setPreferredWidth(200);
table.getColumnModel().getColumn(1).setPreferredWidth(200);
table.getColumnModel().getColumn(2).setPreferredWidth(200);
table.getColumnModel().getColumn(3).setPreferredWidth(200);
Dimension d = table.getPreferredSize();
d.height = 350;
table.setPreferredScrollableViewportSize(d);
table.getTableHeader().setFocusable(true);
return new JScrollPane(table);
}
/*
* When the LAF is changed we need to reset all the items
*/
private void resetComponents() {
models.clear();
((DefaultTableModel) table.getModel()).setRowCount(0);
Vector<String> comboBoxItems = new Vector<String>(50);// buildItemsMap();
UIDefaults defaults = UIManager.getLookAndFeelDefaults();
for (Object key : defaults.keySet()) { // All Swing components will have a UI property.
String componentName = checkForUIKey(key.toString());
if (componentName != null) {
comboBoxItems.add(componentName);
}
}
Collections.sort(comboBoxItems);
comboBox.removeItemListener(this);
comboBox.setModel(new DefaultComboBoxModel(comboBoxItems));
comboBox.setSelectedIndex(-1);
comboBox.addItemListener(this);
comboBox.requestFocusInWindow();
if (selectedItem != null) {
comboBox.setSelectedItem(selectedItem);
}
}
/**
* Create menu bar
*/
private JMenuBar createMenuBar() {
JMenuBar menuBar1 = new JMenuBar();
menuBar1.add(createFileMenu());
menuBar1.add(createLAFMenu());
return menuBar1;
}
/**
* Create menu items for the Application menu
*/
private JMenu createFileMenu() {
JMenu menu = new JMenu("Application");
menu.setMnemonic('A');
menu.addSeparator();
menu.add(new ExitAction());
return menu;
}
/**
* Create menu items for the Look & Feel menu
*/
private JMenu createLAFMenu() {
ButtonGroup bg = new ButtonGroup();
JMenu menu = new JMenu("Look & Feel");
menu.setMnemonic('L');
String lafId = UIManager.getLookAndFeel().getID();
UIManager.LookAndFeelInfo[] lafInfo = UIManager.getInstalledLookAndFeels();
for (int i = 0; i < lafInfo.length; i++) {
String laf = lafInfo[i].getClassName();
String name = lafInfo[i].getName();
Action action = new ChangeLookAndFeelAction(laf, name);
JRadioButtonMenuItem mi = new JRadioButtonMenuItem(action);
menu.add(mi);
bg.add(mi);
if (name.equals(lafId)) {
mi.setSelected(true);
}
}
return menu;
}
/*
* Implement the ItemListener interface
*/
#Override
public void itemStateChanged(ItemEvent e) {
String componentName = (String) e.getItem();
changeTableModel(getClassName(componentName));
selectedItem = componentName;
}
/*
* Use the component name to build the class name
*/
private String getClassName(String componentName) {
if (componentName.equals("TableHeader")) {// The table header is in a child package
return PACKAGE + "table.JTableHeader";
} else {
return PACKAGE + "J" + componentName;
}
}
/*
* Change the TabelModel in the table for the selected component
*/
private void changeTableModel(String className) {
DefaultTableModel model = models.get(className); // Check if we have already built the table model for this component
if (model != null) {
table.setModel(model);
return;
}
model = new DefaultTableModel(COLUMN_NAMES, 0); // Create an empty table to start with
table.setModel(model);
models.put(className, model);
JComponent component = null; // Create an instance of the component so we can get the default Action map and Input maps
try {
if (className.endsWith("JFileChooser")) {// Hack so I don't have to sign the jar file for usage in Java Webstart
component = new JFileChooser(new DummyFileSystemView());
} else {
Object o = Class.forName(className).newInstance();
component = (JComponent) o;
}
} catch (Exception e) {
Object[] row = {e.toString(), "", "", ""};
model.addRow(row);
return;
}
ActionMap actionMap = component.getActionMap(); // Not all components have Actions defined
Object[] keys = actionMap.allKeys();
if (keys == null) {
Object[] row = {"No actions found", "", "", ""};
model.addRow(row);
return;
}
// In some ActionMaps a key of type Object is found (I have no idea why)
// which causes a ClassCastExcption when sorting so we will ignore it
// by converting that entry to the empty string
for (int i = 0; i < keys.length; i++) {
Object key = keys[i];
if (key instanceof String) {
continue;
} else {
keys[i] = "";
}
}
Arrays.sort(keys);
for (int i = 0; i < keys.length; i++) { // Create a new row in the model for every Action found
Object key = keys[i];
if (key != "") {
Object[] row = {key, "", "", ""};
model.addRow(row);
}
}
// Now check each InputMap to see if a KeyStroke is bound the the Action
updateModelForInputMap(model, 1, component.getInputMap(JComponent.WHEN_FOCUSED));
updateModelForInputMap(model, 2, component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW));
updateModelForInputMap(model, 3, component.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT));
}
/*
* The model is potentially update for each of the 3 different InputMaps
*/
private void updateModelForInputMap(TableModel model, int column, InputMap inputMap) {
if (inputMap == null) {
return;
}
KeyStroke[] keys = inputMap.allKeys();
if (keys == null) {
return;
}
// The InputMap is keyed by KeyStroke, however we want to be able to
// access the action names that are bound to a KeyStroke so we will create
// a Hashtble that is keyed by action name.
// Note that multiple KeyStrokes can be bound to the same action name.
Hashtable<Object, String> actions = new Hashtable<Object, String>(keys.length);
for (int i = 0; i < keys.length; i++) {
KeyStroke key = keys[i];
Object actionName = inputMap.get(key);
Object value = actions.get(actionName);
if (value == null) {
actions.put(actionName, key.toString().replace("pressed ", ""));
} else {
actions.put(actionName, value + ", " + key.toString().replace("pressed ", ""));
}
}
for (int i = 0; i < model.getRowCount(); i++) {
// Now we can update the model for those actions that have KeyStrokes mapped to them
String o = actions.get(model.getValueAt(i, 0));
if (o != null) {
model.setValueAt(o.toString(), i, column);
}
}
}
/*
* Change the LAF and recreate the UIManagerDefaults so that the properties
* of the new LAF are correctly displayed.
*/
private class ChangeLookAndFeelAction extends AbstractAction {
private static final long serialVersionUID = 1L;
private String laf;
protected ChangeLookAndFeelAction(String laf, String name) {
this.laf = laf;
putValue(Action.NAME, name);
putValue(Action.SHORT_DESCRIPTION, getValue(Action.NAME));
}
#Override
public void actionPerformed(ActionEvent e) {
try {
JMenuItem mi = (JMenuItem) e.getSource();
JPopupMenu popup = (JPopupMenu) mi.getParent();
JRootPane rootPane = SwingUtilities.getRootPane(popup.getInvoker());
Component c = rootPane.getContentPane().getComponent(0);
rootPane.getContentPane().remove(c);
UIManager.setLookAndFeel(laf);
KeyBindings bindings = new KeyBindings();
rootPane.getContentPane().add(bindings.getContentPane());
SwingUtilities.updateComponentTreeUI(rootPane);
rootPane.requestFocusInWindow();
} catch (Exception ex) {
System.out.println("Failed loading L&F: " + laf);
System.out.println(ex);
}
}
}
private class ExitAction extends AbstractAction {
private static final long serialVersionUID = 1L;
public ExitAction() {
putValue(Action.NAME, "Exit");
putValue(Action.SHORT_DESCRIPTION, getValue(Action.NAME));
putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_X));
}
#Override
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
}
private class DummyFileSystemView extends FileSystemView {
#Override
public File createNewFolder(File containingDir) {
return null;
}
#Override
public File getDefaultDirectory() {
return null;
}
#Override
public File getHomeDirectory() {
return null;
}
}
private static void createAndShowGUI() {
KeyBindings application = new KeyBindings();
JFrame.setDefaultLookAndFeelDecorated(true);
JFrame frame = new JFrame("Key Bindings");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setJMenuBar(application.getMenuBar());
frame.getContentPane().add(application.getContentPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
//UIManager.put("swing.boldMetal", Boolean.FALSE);
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
createAndShowGUI();
}
});
}
}
If you want to run "global events" on your application regardless of where you your current focus is, you'll need to work with the KeyboardFocusManager :
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
kfm.addKeyEventDispatcher(new KeyEventDispatcher() {
#Override
public boolean dispatchKeyEvent(KeyEvent e) {
// do your stuff here
return done;
}
});
Hope this helps.