I have a JTable inside a JPanel. I can scroll up and down the JPanel using the mouse's scroll wheel, but when my mouse is hovering over the JTable, I have to move it out of the table to scroll back up the JPanel using the scroll wheel. Is there a way I can scroll up and down the JPanel using the scroll wheel if the mouse is hovering over the JTable?
I took Xeon's advice in the comment above and implemented a mouse wheel listener that forwards mouse wheel events to the parent component. See the code below.
public class CustomMouseWheelListener implements MouseWheelListener {
private JScrollBar bar;
private int previousValue = 0;
private JScrollPane parentScrollPane;
private JScrollPane customScrollPane;
/** #return The parent scroll pane, or null if there is no parent. */
private JScrollPane getParentScrollPane() {
if (this.parentScrollPane == null) {
Component parent = this.customScrollPane.getParent();
while (!(parent instanceof JScrollPane) && parent != null) {
parent = parent.getParent();
}
this.parentScrollPane = (JScrollPane) parent;
}
return this.parentScrollPane;
}
/**
* Creates a new CustomMouseWheelListener.
* #param customScrollPane The scroll pane to which this listener belongs.
*/
public CustomMouseWheelListener(JScrollPane customScrollPane) {
ValidationUtils.checkNull(customScrollPane);
this.customScrollPane = customScrollPane;
this.bar = this.customScrollPane.getVerticalScrollBar();
}
/** {#inheritDoc} */
#Override
public void mouseWheelMoved(MouseWheelEvent event) {
JScrollPane parent = getParentScrollPane();
if (parent != null) {
if (event.getWheelRotation() < 0) {
if (this.bar.getValue() == 0 && this.previousValue == 0) {
parent.dispatchEvent(cloneEvent(event));
}
}
else {
if (this.bar.getValue() == getMax() && this.previousValue == getMax()) {
parent.dispatchEvent(cloneEvent(event));
}
}
this.previousValue = this.bar.getValue();
}
else {
this.customScrollPane.removeMouseWheelListener(this);
}
}
/** #return The maximum value of the scrollbar. */
private int getMax() {
return this.bar.getMaximum() - this.bar.getVisibleAmount();
}
/**
* Copies the given MouseWheelEvent.
*
* #param event The MouseWheelEvent to copy.
* #return A copy of the mouse wheel event.
*/
private MouseWheelEvent cloneEvent(MouseWheelEvent event) {
return new MouseWheelEvent(getParentScrollPane(), event.getID(), event.getWhen(),
event.getModifiers(), 1, 1, event.getClickCount(), false, event.getScrollType(),
event.getScrollAmount(), event.getWheelRotation());
}
}
Thanks denshaotoko for sharing your code. I've implemented a solution along the same lines (event forwarding) but put it into the scroll pane directly. Thought it might be useful to others.
/**
* Scroll pane that only scrolls when it owns focus. When not owning focus (i.e. mouse
* hover), propagates mouse wheel events to its container.
* <p>
* This is a solution for <i>"I have a JTable inside a JPanel. When my mouse is hovering
* over the JTable, I have to move it out of the table to scroll the JPanel."</i>
*/
public class ScrollWhenFocusedPane extends JScrollPane {
// Note: don't leave users with "scroll on focus" behaviour
// on widgets that they cannot focus. These will be okay.
public ScrollWhenFocusedPane (JTree view) {super (view);}
public ScrollWhenFocusedPane (JList view) {super (view);}
public ScrollWhenFocusedPane (JTable view) {super (view);}
public ScrollWhenFocusedPane (JTextArea view) {super (view);}
#Override
protected void processMouseWheelEvent (MouseWheelEvent evt) {
Component outerWidget = SwingUtilities.getAncestorOfClass (Component.class, this);
// Case 1: we don't have focus, so we don't scroll
Component innerWidget = getViewport().getView();
if (!innerWidget.hasFocus())
outerWidget.dispatchEvent(evt);
// Case 2: we have focus
else {
JScrollBar innerBar = getVerticalScrollBar();
if (!innerBar.isShowing()) // Deal with horizontally scrolling widgets
innerBar = getHorizontalScrollBar();
boolean wheelUp = evt.getWheelRotation() < 0;
boolean atTop = (innerBar.getValue() == 0);
boolean atBottom = (innerBar.getValue() == (innerBar.getMaximum() - innerBar.getVisibleAmount()));
// Case 2.1: we've already scrolled as much as we could
if ((wheelUp & atTop) || (!wheelUp & atBottom))
outerWidget.dispatchEvent(evt);
// Case 2.2: we'll scroll
else
super.processMouseWheelEvent (evt);
}
}
}
Related
The Swing Text components, all of which extend JTextComponent, when the user selects some text, the JTextComponent class delegates the work of handling selected text to an instance of the Caret interface, called DefaultCaret. This interface not only shows the blinking caret, it keeps track of whatever text the user has selected, and response to mouse and keyboard events that changes the selection range.
The Swing DefaultCaret has most of the behavior of a standard caret, but some of my high-end users have pointed out what it doesn't do. Here are their issues:
(Note: These examples have trouble in Microsoft Edge because, when you select text, it puts up a "..." menu. In these examples, if you're using Edge, you need to type the escape key to get rid of that menu before going on to the next step.)
If I double-click on a word, it should select the entire word. Java Swing's Caret does this. But, after doubling-clicking on a word, if I then try to extend the selection by shift-clicking on a second word, a standard caret extends the selection to include the entire second word. To illustrate, in the example text below, if I double-click after the o in clock, it selects the word clock, as it should. But if I then hold down the shift key and click after the o in wound, it should extend the selection all the way to the d. It does so on this page, but not in Java Swing. In Swing, it still extends the selection, but only to the location of the mouse-click.
Example: The clock has been wound too tight.
If I try to select a block of text by doing a full click, then drag, it should extend the selection by an entire word at a time as I drag through the text. (By "full click, then drag, I mean the following events done quickly on the same spot: mouseDown, mouseUp, mouseDown, mouseMove. This is like a double-click without the final mouse-up event.) You can try it on this page, and it will work, but it won't work in Java Swing. In Swing it will still extend the selection, but only to the position of the mouse.
If I triple-click on some text, it will select the whole paragraph. (This doesn't work in Microsoft Edge, but it works in most browsers and editors) This doesn't work in Swing.
If, after triple-clicking to select a paragraph, I then do a shift-click on a different paragraph, it should extend the selection to include the entire new paragraph.
Like the full-click and drag example in item 2, if you do a full double-click and drag, it should first select the entire paragraph, then extend the selection one paragraph at a time. (Again, this doesn't work in Edge.) This behavior is less standard than the others, but it's still pretty common.
Some of my power users want to be able to use these in the Java Swing application that I maintain, and I want to give it to them. The whole point of my application is to speed up the processing of their data, and these changes will help with that. How can I do this?
Here's a class I wrote to address these issues. It addresses issues 1 and 2. As far as triple-clicks go, it doesn't change the behavior of selecting the line instead the paragraph, but it addresses issue 4 on the selected line, consistent with the triple-click behavior. It doesn't address issue 5.
This class also has two factory methods that will do trouble-free installations of this caret into your text components.
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeListener;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.DefaultCaret;
import javax.swing.text.JTextComponent;
import javax.swing.text.Position;
import javax.swing.text.Utilities;
/**
* <p>Implements Standard rules for extending the selection, consistent with the standard
* behavior for extending the selection in all word processors, browsers, and other text
* editing tools, on all platforms. Without this, Swing's behavior on extending the
* selection is inconsistent with all other text editing tools.
* </p><p>
* Swing components don't handle selectByWord the way most UI text components do. If you
* double-click on a word, they will all select the entire word. But if you do a
* click-and-drag, most components will (a) select the entire clicked word, and
* (b) extend the selection a word at a time as the user drags across the text. And if
* you double- click on a word and follow that with a shift-click, most components will
* also extend the selection a word at a time. Swing components handle a double-clicked
* word the standard way, but do not handle click-and-drag or shift-click correctly. This
* caret, which replaces the standard DefaultCaret, fixes this.</p>
* <p>Created by IntelliJ IDEA.</p>
* <p>Date: 2/23/20</p>
* <p>Time: 10:58 PM</p>
*
* #author Miguel Mu\u00f1oz
*/
public class StandardCaret extends DefaultCaret {
// In the event of a double-click, these are the positions of the low end and high end
// of the word that was clicked.
private int highMark;
private int lowMark;
private boolean selectingByWord = false; // true when the last selection was done by word.
private boolean selectingByRow = false; // true when the last selection was done by paragraph.
/**
* Instantiate an EnhancedCaret.
*/
public StandardCaret() {
super();
}
/**
* <p>Install this Caret into a JTextComponent. Carets may not be shared among multiple
* components.</p>
* #param component The component to use the EnhancedCaret.
*/
public void installInto(JTextComponent component) {
replaceCaret(component, this);
}
/**
* Install a new StandardCaret into a JTextComponent, such as a JTextField or
* JTextArea, and starts the Caret blinking using the same blink-rate as the
* previous Caret.
*
* #param component The JTextComponent subclass
*/
public static void installStandardCaret(JTextComponent component) {
replaceCaret(component, new StandardCaret());
}
/**
* Installs the specified Caret into the JTextComponent, and starts the Caret blinking
* using the same blink-rate as the previous Caret. This works with any Caret
*
* #param component The text component to get the new Caret
* #param caret The new Caret to install
*/
public static void replaceCaret(final JTextComponent component, final Caret caret) {
final Caret priorCaret = component.getCaret();
int blinkRate = priorCaret.getBlinkRate();
if (priorCaret instanceof PropertyChangeListener) {
// For example, com.apple.laf.AquaCaret, the troublemaker, installs this listener
// which doesn't get removed when the Caret gets uninstalled.
component.removePropertyChangeListener((PropertyChangeListener) priorCaret);
}
component.setCaret(caret);
caret.setBlinkRate(blinkRate); // Starts the new caret blinking.
}
#Override
public void mousePressed(final MouseEvent e) {
// if user is doing a shift-click. Construct a new MouseEvent that happened at one
// end of the word, and send that to super.mousePressed().
boolean isExtended = isExtendSelection(e);
if (selectingByWord && isExtended) {
MouseEvent alternateEvent = getRevisedMouseEvent(e, Utilities::getWordStart, Utilities::getWordEnd);
super.mousePressed(alternateEvent);
} else if (selectingByRow && isExtended) {
MouseEvent alternateEvent = getRevisedMouseEvent(e, Utilities::getRowStart, Utilities::getRowEnd);
super.mousePressed(alternateEvent);
} else {
if (!isExtended) {
int clickCount = e.getClickCount();
selectingByWord = clickCount == 2;
selectingByRow = clickCount == 3;
}
super.mousePressed(e); // let the system select the clicked word
// save the low end of the selected word.
lowMark = getMark();
if (selectingByWord || selectingByRow) {
// User did a double- or triple-click...
// They've selected the whole word. Record the high end.
highMark = getDot();
} else {
// Not a double-click.
highMark = lowMark;
}
}
}
#Override
public void mouseClicked(final MouseEvent e) {
super.mouseClicked(e);
if (selectingByRow) {
int mark = getMark();
int dot = getDot();
lowMark = Math.min(mark, dot);
highMark = Math.max(mark, dot);
}
}
private MouseEvent getRevisedMouseEvent(final MouseEvent e, final BiTextFunction getStart, final BiTextFunction getEnd) {
int newPos;
int pos = getPos(e);
final JTextComponent textComponent = getComponent();
try {
if (pos > highMark) {
newPos = getEnd.loc(textComponent, pos);
setDot(lowMark);
} else if (pos < lowMark) {
newPos = getStart.loc(textComponent, pos);
setDot(highMark);
} else {
if (getMark() == lowMark) {
newPos = getEnd.loc(textComponent, pos);
} else {
newPos = getStart.loc(textComponent, pos);
}
pos = -1; // ensure we make a new event
}
} catch (BadLocationException ex) {
throw new IllegalStateException(ex);
}
MouseEvent alternateEvent;
if (newPos == pos) {
alternateEvent = e;
} else {
alternateEvent = makeNewEvent(e, newPos);
}
return alternateEvent;
}
private boolean isExtendSelection(MouseEvent e) {
// We extend the selection when the shift is down but control is not. Other modifiers don't matter.
int modifiers = e.getModifiersEx();
int shiftAndControlDownMask = MouseEvent.SHIFT_DOWN_MASK | MouseEvent.CTRL_DOWN_MASK;
return (modifiers & shiftAndControlDownMask) == MouseEvent.SHIFT_DOWN_MASK;
}
#Override
public void setDot(final int dot, final Position.Bias dotBias) {
super.setDot(dot, dotBias);
}
#Override
public void mouseDragged(final MouseEvent e) {
if (!selectingByWord && !selectingByRow) {
super.mouseDragged(e);
} else {
BiTextFunction getStart;
BiTextFunction getEnd;
if (selectingByWord) {
getStart = Utilities::getWordStart;
getEnd = Utilities::getWordEnd;
} else {
// selecting by paragraph
getStart = Utilities::getRowStart;
getEnd = Utilities::getRowEnd;
}
// super.mouseDragged just calls moveDot() after getting the position. We can do
// the same thing...
// There's no "setMark()" method. You can set the mark by calling setDot(). It sets
// both the mark and the dot to the same place. Then you can call moveDot() to put
// the dot somewhere else.
if ((!e.isConsumed()) && SwingUtilities.isLeftMouseButton(e)) {
int pos = getPos(e);
JTextComponent component = getComponent();
try {
if (pos > highMark) {
int wordEnd = getEnd.loc(component, pos);
setDot(lowMark);
moveDot(wordEnd);
} else if (pos < lowMark) {
int wordStart = getStart.loc(component, pos);
setDot(wordStart); // Sets the mark, too
moveDot(highMark);
} else {
setDot(lowMark);
moveDot(highMark);
}
} catch (BadLocationException ex) {
ex.printStackTrace();
}
}
}
}
private int getPos(final MouseEvent e) {
JTextComponent component = getComponent();
Point pt = new Point(e.getX(), e.getY());
Position.Bias[] biasRet = new Position.Bias[1];
return component.getUI().viewToModel(component, pt, biasRet);
}
private MouseEvent makeNewEvent(MouseEvent e, int pos) {
JTextComponent component = getComponent();
try {
Rectangle rect = component.getUI().modelToView(component, pos);
return new MouseEvent(
component,
e.getID(),
e.getWhen(),
e.getModifiers(),
rect.x,
rect.y,
e.getClickCount(),
e.isPopupTrigger(),
e.getButton()
);
} catch (BadLocationException ev) {
ev.printStackTrace();
throw new IllegalStateException(ev);
}
}
// For eventual use by a "select paragraph" feature:
// private static final char NEW_LINE = '\n';
// private static int getParagraphStart(JTextComponent component, int position) {
// return component.getText().substring(0, position).lastIndexOf(NEW_LINE);
// }
//
// private static int getParagraphEnd(JTextComponent component, int position) {
// return component.getText().indexOf(NEW_LINE, position);
// }
/**
* Don't use this. I should throw CloneNotSupportedException, but it won't compile if I
* do. Changing the access to protected doesn't help. If you don't believe me, try it
* yourself.
* #return A bad clone of this.
*/
#SuppressWarnings({"CloneReturnsClassType", "UseOfClone"})
#Override
public Object clone() {
return super.clone();
}
#FunctionalInterface
private interface BiTextFunction {
int loc(JTextComponent component, int position) throws BadLocationException;
}
}
I need help implementing my own traversal policy.
My goal was to be able to traverse with tab some of the components in a panel, not all of them.
I was able to research the FocusTraversalPolicy class and figure out a little how this works. The code below shows how far I got. But I seem to have hit a wall.
This code will allow me to traverse from one component to another, but there are in my Container smaller jpanels with jtextfields in them. For some reason, I am not able to traverse to these using tab, even though I added the jpanels into my policy.
I am aware that I can just access the textfields inside these jpanels and add them to my policy. However, since these smaller jpanels are sort of dynamic and can add extra components inside of them, I am looking more for something that will allow me to pass from the Parent container traversal cycle to the smaller one.
I could be wrong, but I believe I should add something to the getComponentAfter(...) and the getComponentBefore(...) methods. As you can see, I commented out some of the things I was trying. I would appreciate any help you can give me. I attached a picture of my panel so you can see what I mean.
public class TabFocusAdapter extends FocusTraversalPolicy implements FocusListener, MouseListener {
private ArrayList<Component> comps = new ArrayList<Component>();
private int focus, def;
private boolean mouse_focus = false;
public TabFocusAdapter(int f) {
focus = def = f;
}
#Override
/***Returns the component after the current one
* if current one is the last in the cycle, it will return the first
* it will skip all disabled components */
public Component getComponentAfter(Container aContainer, Component aComponent) {
//if(((Container) aComponent).getFocusTraversalPolicy()!=null)
//if(aComponent)
//return ((Container)aComponent).getFocusTraversalPolicy().getFirstComponent((Container) aComponent);
focus++;
if(focus == comps.size()) focus = 0;
Component comp = comps.get(focus);
while(!comp.isEnabled() || !comp.isShowing() || !comp.isVisible()) {
focus++;
if(focus == comps.size()) focus = 0;
comp = comps.get(focus);
}
return comps.get(focus);
}
#Override
/**Returns the component before the current one
* if current one is the first in the cycle, it will return the last
* it will skip all disabled components */
public Component getComponentBefore(Container aContainer, Component aComponent) {
focus--;
if(focus == -1) focus = comps.size()-1;
Component comp = comps.get(focus);
while(!comp.isEnabled() || !comp.isShowing() || !comp.isVisible()) {
focus--;
if(focus == -1) focus = comps.size()-1;
comp = comps.get(focus);
}
return comps.get(focus);
}
#Override
public Component getFirstComponent(Container aContainer) {
return comps.get(0);
}
#Override
public Component getLastComponent(Container aContainer) {
return comps.get(comps.size() - 1);
}
#Override
/**Returns the starting component */
public Component getDefaultComponent(Container aContainer) {
return comps.get(def);
}
public void addComp(Component comp) {
comps.add(comp);
comp.addMouseListener(this);
comp.addFocusListener(this);
}
#Override
public void focusGained(FocusEvent e) {
Component comp = e.getComponent();
if(!mouse_focus && comp instanceof JTextComponent)
((JTextComponent)comp).selectAll();
}
#Override
public void mousePressed(MouseEvent e) {
mouse_focus = true;
focus = comps.indexOf(e.getComponent());
}
#Override
public void mouseReleased(MouseEvent e) {
mouse_focus = false;
}
#Override
public void mouseClicked(MouseEvent e) {}
#Override
public void focusLost(FocusEvent e) {}
#Override
public void mouseEntered(MouseEvent e) {}
#Override
public void mouseExited(MouseEvent e) {}
}
I solved my problem by including a series of checks into the getComponentAfter(...) and getComponentBefore(...) methods. I am including my code below.
Basically, to determine whether to move to a lower cycle, it asks whether the next component is a Focus Cycle Root.
When concluding a cycle, to determine whether to move to an upper cycle, it asks whether the parent of the container is a Focus Traversal Policy Provider.
I got the general idea for this solution by looking at how these methods where implemented by the default traversal policies of my JFrame.
'''
public Component getComponentAfter(Container aContainer, Component aComponent) {
Component comp;
do{
focus++;
if(focus == comps.size()) {
focus = 0;
ancestor = aContainer;
do ancestor = ancestor.getParent();
while(ancestor!=null && !ancestor.isFocusTraversalPolicyProvider());
if(ancestor != null && ancestor.isFocusTraversalPolicyProvider())
return (ancestor
.getFocusTraversalPolicy()
.getComponentAfter(ancestor, aContainer));
}
comp = comps.get(focus);
} while(!comp.isEnabled() || !comp.isShowing() || !comp.isVisible());
if(comp instanceof Container && ((Container)comp).isFocusCycleRoot()) {
return ((Container)comp)
.getFocusTraversalPolicy()
.getDefaultComponent((Container) comp);
}
return comp;
}
'''
I just have a very basic question, I am wondering what would be the best way to make a stack of cards where each are offset. In the code below I tried to use a StackPane, the problem is however that it doesn't seem to register the offset coordinates.
In the image below you will see that I tried to deal 2 cards to the dealer and to player1 and yes those are two different stacks (Note: I haven't taken the time to position them correctly yet, also the border around the table is temporary). I tried manually loading the cards and I get the same result, it doesn't offset the cards so it appears as though only one card is on the table:
public class PlayerTableCardPane extends Pane {
// Data Fields
ImageView[] cardBack;
private int cardXOffset;
private int cardHeight;
private int cardCount;
// StackPane for cards
StackPane cardStack;
/** Constructor */
public PlayerTableCardPane() {
this(50, 5);
}
/** Constructor for setting custom height */
public PlayerTableCardPane(int cardHeight, int cardXOffset) {
this.cardStack = new StackPane();
this.cardBack = new ImageView[5];
this.cardXOffset = cardXOffset;
this.cardHeight = cardHeight;
this.cardCount = 0;
for (int index = 0; index < 5; index++) {
Image newImage = new Image("image/cardDownSm.png");
ImageView setCardBack = new ImageView();
setCardBack.setImage(newImage);
setCardBack.setVisible(false);
setCardBack.setFitHeight(cardHeight);
setCardBack.setPreserveRatio(true);
setCardBack.setSmooth(true);
// Offset each card
setCardBack.setX(index * cardXOffset);
this.cardBack[index] = setCardBack;
}
this.cardStack.getChildren().addAll(this.cardBack);
getChildren().add(this.cardStack);
}
/** Get cardXOffset */
public int getCardXOffset() {
return this.cardXOffset;
}
/** Set cardXOffset */
public void setCardXOffset(int cardXOffset) {
this.cardXOffset = cardXOffset;
}
/** Get cardCount */
public int getCardCount() {
return this.cardCount;
}
/** Set cardCount */
public void setCardCount(int cardCount) {
this.cardCount = cardCount;
}
/** Load cards into StackPane */
public void loadCard() {
if (cardCount == 0) {
cardBack[0].setVisible(true);
} else if (cardCount == 1) {
cardBack[1].setVisible(true);
} else if (cardCount == 2) {
cardBack[2].setVisible(true);
} else if (cardCount == 3) {
cardBack[3].setVisible(true);
} else if (cardCount == 4) {
cardBack[4].setVisible(true);
}
this.cardCount++;
}
/** Hide last card() */
public void hideLastCard() {
if (cardCount == 4) {
cardBack[4].setVisible(false);
} else if (cardCount == 3) {
cardBack[3].setVisible(false);
} else if (cardCount == 2) {
cardBack[2].setVisible(false);
} else if (cardCount == 1) {
cardBack[1].setVisible(false);
} else if (cardCount == 0) {
cardBack[0].setVisible(false);
}
this.cardCount--;
}
/** Replace card with FaceUp Image */
public void setCardImg(ImageView card, int cardPos) {
cardBack[cardPos].setImage(card.getImage());
}
}
StackPane lays out its child Nodes according just to the alignment property of the StackPane itself: the layoutX and layoutY of the Node are basically ignored. The default value of the alignment property is CENTER. Your best solution here is just to use a Pane instead of a StackPane.
You could also potentially solve this by setting the translateX property of the images to be placed in the StackPane. However, note that such transforms are not included in layout calculations, so you introduce the possibility of pushing the cards out of the visible bounds of the StackPane.
I've created a custom TreeModel by extending DefaultTreeModel, so the user can rename nodes in my JTree. This works fine if my user inputs a new name and then hits enter. If instead of hitting enter, the user clicks away from the node then my valueForPathChanged method doesn't fire and I can't get the new String. How can I get the new user String without the user hitting enter and instead clicking somewhere else in the Tree/Panel/Frame?
To improve the situation slightly you can set the invokesStopCellEditing property of the JTree: being true the ui will commit a pending edit on some internal changes, like expansion or selection change.
final JTree tree = new JTree();
tree.setEditable(true);
// this will often help (see its api doc), but no guarantee
tree.setInvokesStopCellEditing(true);
// a focusListener is **not** helping
FocusListener l = new FocusListener() {
#Override
public void focusGained(FocusEvent e) {
}
#Override
public void focusLost(FocusEvent e) {
// this would prevent editing at all
// tree.stopEditing();
}
};
tree.addFocusListener(l);
JComponent panel = new JPanel(new BorderLayout());
panel.add(new JScrollPane(tree));
panel.add(new JButton("just something to focus"), BorderLayout.SOUTH);
The snippet (to play with) also demonstrates that a focusListener is not working.
CellEditorRemover and its usage in JXTree (as you see, there's slightly more to add than the bare remover (which basically is-a listener to KeyboardFocusManager's focusOwner property):
/**
* {#inheritDoc} <p>
* Overridden to fix focus issues with editors.
* This method installs and updates the internal CellEditorRemover which
* terminates ongoing edits if appropriate. Additionally, it
* registers a CellEditorListener with the cell editor to grab the
* focus back to tree, if appropriate.
*
* #see #updateEditorRemover()
*/
#Override
public void startEditingAtPath(TreePath path) {
super.startEditingAtPath(path);
if (isEditing()) {
updateEditorListener();
updateEditorRemover();
}
}
/**
* Hack to grab focus after editing.
*/
private void updateEditorListener() {
if (editorListener == null) {
editorListener = new CellEditorListener() {
#Override
public void editingCanceled(ChangeEvent e) {
terminated(e);
}
/**
* #param e
*/
private void terminated(ChangeEvent e) {
analyseFocus();
((CellEditor) e.getSource()).removeCellEditorListener(editorListener);
}
#Override
public void editingStopped(ChangeEvent e) {
terminated(e);
}
};
}
getCellEditor().addCellEditorListener(editorListener);
}
/**
* This is called from cell editor listener if edit terminated.
* Trying to analyse if we should grab the focus back to the
* tree after. Brittle ... we assume we are the first to
* get the event, so we can analyse the hierarchy before the
* editing component is removed.
*/
protected void analyseFocus() {
if (isFocusOwnerDescending()) {
requestFocusInWindow();
}
}
/**
* Returns a boolean to indicate if the current focus owner
* is descending from this table.
* Returns false if not editing, otherwise walks the focusOwner
* hierarchy, taking popups into account. <p>
*
* PENDING: copied from JXTable ... should be somewhere in a utility
* class?
*
* #return a boolean to indicate if the current focus
* owner is contained.
*/
private boolean isFocusOwnerDescending() {
if (!isEditing()) return false;
Component focusOwner =
KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
// PENDING JW: special casing to not fall through ... really wanted?
if (focusOwner == null) return false;
if (SwingXUtilities.isDescendingFrom(focusOwner, this)) return true;
// same with permanent focus owner
Component permanent =
KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
return SwingXUtilities.isDescendingFrom(permanent, this);
}
/**
* Overridden to release the CellEditorRemover, if any.
*/
#Override
public void removeNotify() {
if (editorRemover != null) {
editorRemover.release();
editorRemover = null;
}
super.removeNotify();
}
/**
* Lazily creates and updates the internal CellEditorRemover.
*
*
*/
private void updateEditorRemover() {
if (editorRemover == null) {
editorRemover = new CellEditorRemover();
}
editorRemover.updateKeyboardFocusManager();
}
/** This class tracks changes in the keyboard focus state. It is used
* when the JXTree is editing to determine when to terminate the edit.
* If focus switches to a component outside of the JXTree, but in the
* same window, this will terminate editing. The exact terminate
* behaviour is controlled by the invokeStopEditing property.
*
* #see javax.swing.JTree#setInvokesStopCellEditing(boolean)
*
*/
public class CellEditorRemover implements PropertyChangeListener {
/** the focusManager this is listening to. */
KeyboardFocusManager focusManager;
public CellEditorRemover() {
updateKeyboardFocusManager();
}
/**
* Updates itself to listen to the current KeyboardFocusManager.
*
*/
public void updateKeyboardFocusManager() {
KeyboardFocusManager current = KeyboardFocusManager.getCurrentKeyboardFocusManager();
setKeyboardFocusManager(current);
}
/**
* stops listening.
*
*/
public void release() {
setKeyboardFocusManager(null);
}
/**
* Sets the focusManager this is listening to.
* Unregisters/registers itself from/to the old/new manager,
* respectively.
*
* #param current the KeyboardFocusManager to listen too.
*/
private void setKeyboardFocusManager(KeyboardFocusManager current) {
if (focusManager == current)
return;
KeyboardFocusManager old = focusManager;
if (old != null) {
old.removePropertyChangeListener("permanentFocusOwner", this);
}
focusManager = current;
if (focusManager != null) {
focusManager.addPropertyChangeListener("permanentFocusOwner",
this);
}
}
#Override
public void propertyChange(PropertyChangeEvent ev) {
if (!isEditing()) {
return;
}
Component c = focusManager.getPermanentFocusOwner();
JXTree tree = JXTree.this;
while (c != null) {
if (c instanceof JPopupMenu) {
c = ((JPopupMenu) c).getInvoker();
} else {
if (c == tree) {
// focus remains inside the table
return;
} else if ((c instanceof Window) ||
(c instanceof Applet && c.getParent() == null)) {
if (c == SwingUtilities.getRoot(tree)) {
if (tree.getInvokesStopCellEditing()) {
tree.stopEditing();
}
if (tree.isEditing()) {
tree.cancelEditing();
}
}
break;
}
c = c.getParent();
}
}
}
}
you can add an anonymous instance of FocusListener
and implement
void focusLost(FocusEvent e)
this gets triggered before the value is saved so will be not help you in getting the last value. Instead you should set
myTree.setInvokesStopCellEditing(true);
that fires a property change for the INVOKES_STOP_CELL_EDITING_PROPERTY, which
means that you need to have in your tree model something like
public void valueForPathChanged(TreePath path, Object newValue)
{
AdapterNode node = (AdapterNode)
path.getLastPathComponent();
node.getDomNode().setNodeValue((String)newValue);
fireTreeNodesChanged(new TreeModelEvent(this,
path));
}
Regards
I have the following "tree" of objects:
JPanel
JScrollPane
JPanel
JPanel
JScrollPane
JTextPane
When using the mouse wheel to scroll over the outer JScrollPane I encounter one annoying problem. As soon as the mouse cursor touches the inner JScrollPane, it seems that the scrolling events get passed into that JScrollPane and are not processed anymore by the first one. That means that scrolling the "parent" JScrollPane stops.
Is it possible to disable only the mouse wheel on the inner JScrollPane? Or even better, disable scrolling if there is nothing to scroll (most of the time the textpane only contains 1-3 lines of text), but enable it if there is more content?
I have run into this annoying problem also, and Sbodd's solution was not acceptable for me because I needed to be able to scroll inside tables and JTextAreas. I wanted the behavior to be the same as a browser, where the mouse over a scrollable control will scroll that control until the control bottoms out, then continue to scroll the parent scrollpane, usually the scrollpane for the whole page.
This class will do just that. Just use it in place of a regular JScrollPane. I hope it helps you.
/**
* A JScrollPane that will bubble a mouse wheel scroll event to the parent
* JScrollPane if one exists when this scrollpane either tops out or bottoms out.
*/
public class PDControlScrollPane extends JScrollPane {
public PDControlScrollPane() {
super();
addMouseWheelListener(new PDMouseWheelListener());
}
class PDMouseWheelListener implements MouseWheelListener {
private JScrollBar bar;
private int previousValue = 0;
private JScrollPane parentScrollPane;
private JScrollPane getParentScrollPane() {
if (parentScrollPane == null) {
Component parent = getParent();
while (!(parent instanceof JScrollPane) && parent != null) {
parent = parent.getParent();
}
parentScrollPane = (JScrollPane)parent;
}
return parentScrollPane;
}
public PDMouseWheelListener() {
bar = PDControlScrollPane.this.getVerticalScrollBar();
}
public void mouseWheelMoved(MouseWheelEvent e) {
JScrollPane parent = getParentScrollPane();
if (parent != null) {
/*
* Only dispatch if we have reached top/bottom on previous scroll
*/
if (e.getWheelRotation() < 0) {
if (bar.getValue() == 0 && previousValue == 0) {
parent.dispatchEvent(cloneEvent(e));
}
} else {
if (bar.getValue() == getMax() && previousValue == getMax()) {
parent.dispatchEvent(cloneEvent(e));
}
}
previousValue = bar.getValue();
}
/*
* If parent scrollpane doesn't exist, remove this as a listener.
* We have to defer this till now (vs doing it in constructor)
* because in the constructor this item has no parent yet.
*/
else {
PDControlScrollPane.this.removeMouseWheelListener(this);
}
}
private int getMax() {
return bar.getMaximum() - bar.getVisibleAmount();
}
private MouseWheelEvent cloneEvent(MouseWheelEvent e) {
return new MouseWheelEvent(getParentScrollPane(), e.getID(), e
.getWhen(), e.getModifiers(), 1, 1, e
.getClickCount(), false, e.getScrollType(), e
.getScrollAmount(), e.getWheelRotation());
}
}
}
Inspired by the existing answers, I
took the code from Nemi's answer
combined it with kleopatra's answer to a similar question to avoid constructing the MouseWheelEvent verbosely
extracted the listener into its own top-level class so that it can be used in contexts where the JScrollPane class cannot be extended
inlined the code as far as possible.
The result is this piece of code:
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
/**
* Passes mouse wheel events to the parent component if this component
* cannot scroll further in the given direction.
* <p>
* This behavior is a little better than Swing's default behavior but
* still worse than the behavior of Google Chrome, which remembers the
* currently scrolling component and sticks to it until a timeout happens.
*
* #see Stack Overflow
*/
public final class MouseWheelScrollListener implements MouseWheelListener {
private final JScrollPane pane;
private int previousValue;
public MouseWheelScrollListener(JScrollPane pane) {
this.pane = pane;
previousValue = pane.getVerticalScrollBar().getValue();
}
public void mouseWheelMoved(MouseWheelEvent e) {
Component parent = pane.getParent();
while (!(parent instanceof JScrollPane)) {
if (parent == null) {
return;
}
parent = parent.getParent();
}
JScrollBar bar = pane.getVerticalScrollBar();
int limit = e.getWheelRotation() < 0 ? 0 : bar.getMaximum() - bar.getVisibleAmount();
if (previousValue == limit && bar.getValue() == limit) {
parent.dispatchEvent(SwingUtilities.convertMouseEvent(pane, e, parent));
}
previousValue = bar.getValue();
}
}
It is used like this:
JScrollPane pane = new JScrollPane();
pane.addMouseWheelListener(new MouseWheelScrollListener(pane));
Once an instance of this class is created and bound to a scroll pane, it cannot be reused for another component since it remembers the previous position of the vertical scroll bar.
Sadly, the obvious solution (JScrollPane.setWheelScrollingEnabled(false)) doesn't actually deregister for MouseWheelEvents, so it doesn't achieve the effect you want.
Here's a crude-hackery way of disabling scrolling altogether that will let the MouseWheelEvents reach the outer JScrollPane:
for (MouseWheelListener mwl : scrollPane.getMouseWheelListeners()) {
scrollPane.removeMouseWheelListener(mwl);
}
If you do this to your inner JScrollPane, it'll never respond to scroll wheel events; the outer JScrollPane will get all of them.
If you want to do it "cleanly", you'd need to implement your own ScrollPaneUI, and set that as the JScrollPane's UI with setUI(). Unfortunately, you can't just extend BasicScrollPaneUI and disable its mouse wheel listener, because the relevant member variables are private and there aren't any flags or guards on the ScrollPaneUI's installation of its MouseWheelListener.
For your "even better" solution, you'd have to dig deeper than I have time to into the ScrollPaneUI, find the hooks where the scrollbars get made visible / invisible, and add/remove your MouseWheelListener at those points.
Hope that helps!
#Nemi has a good solution already.
I boiled it down a bit further, putting the follwing method in my library:
static public void passMouseWheelEventsToParent(final Component pComponent, final Component pParent) {
pComponent.addMouseWheelListener((final MouseWheelEvent pE) -> {
pParent.dispatchEvent(new MouseWheelEvent(pParent, pE.getID(), pE.getWhen(), pE.getModifiers(), 1, 1, pE.getClickCount(), false, pE.getScrollType(), pE.getScrollAmount(), pE.getWheelRotation()));
});
}