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;
}
}
Related
- = UPDATE = -
It turns out the issue was not with Java but with my Apple keyboard. Holding down a letter key brings up a menu that breaks my Java programs. By disabling that menu popup, my KeyListener and my Key Bindings both work as they should. Thank you all for your answers.
Question
I have searched on Google and on StackOverflow for an answer to my question, but to no avail. All of the questions that I've found have the main class extending JComponent, JFrame, JPanel, etc., and not Canvas.
Now for my question:
I am having trouble getting my Java KeyListener to cooperate while my program runs. When I start my program, everything works as usual. However, as I start pressing keys and moving things around (with said keys), the program begins to delay and take more time for the key presses to register. All of a sudden, they KeyListener breaks altogether and I get no input (even a System.out.println statement in the keyPressed method shows no activity). I have three classes that have to do with my KeyListener in any way.
If it helps, the goal of this program is to use BufferedImage class to plot points from different mathematical functions, like a sine wave. I have commented the best I can without being Super-Comment-Man, but I can clarify on the purpose of any code to the best of my ability.
First, my Screen class (draws stuff on the JFrame with a BufferStrategy):
package com.elek.waves.graphics;
import java.awt.Canvas;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import javax.swing.JFrame;
import com.elek.waves.graphics.math.Controller;
import com.elek.waves.graphics.math.Graph;
import com.elek.waves.input.Keyboard;
/**
* The engine of the entire Waves project. Processes the BufferedImage and puts the JFrame
* on the screen. Uses other classes to calculate where to put pixels (what color each pixel
* in the array should be) and get keyboard input.
*
* #author my name
* #version 1.0
*/
public class Screen extends Canvas {
/**
* Holds some *important* number that makes Java happy.
*/
private static final long serialVersionUID = 1L;
/**
* Constant (and static) dimensions of the window.
*/
public static final int WIDTH = 800, HEIGHT = 800;
/**
* Frame that will contain the BufferedImage and all its glory.
*/
private JFrame frame;
/**
* BufferedImage processes the pixel array and translates it into fancy screen magic.
*/
private BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
/**
* Holds color data for each pixel on the screen. Each pixel has an integer value equivalent
* to the hexadecimal RGB value for whatever color this pixel is supposed to be.
*/
private int[] pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
/**
* Graph object to draw the lines on.
*/
private Graph graph;
/**
* Controller object to control the graph.
*/
private Controller controller;
/**
* Keybaord object to use as a key-listener.
*/
private Keyboard key;
/* -- Constructor -- */
/**
* Creates a new Screen object. Initializes the JFrame object.
*/
public Screen() {
frame = new JFrame("Magic!");
graph = new Graph(pixels);
key = new Keyboard();
controller = new Controller(key, graph);
addKeyListener(key);
}
/* -- Methods -- */
/**
* Called once and only once by the main method. Repeatedly calls the update and render methods
* until the program stops running.
*/
private void start() {
this.requestFocus();
this.requestFocusInWindow();
while (true) {
update();
render();
}
}
/**
* Called by the start method repeatedly. First, clears the screen of the previous image in
* order to prevent ghost-imaging or blurring. Then, updates the pixel array to whatever it
* needs to be for the next iteration of the render method.
*/
private void update() {
// Update the keyboard input
key.update();
// Update the controller
controller.update();
// Clean up the screen and then graph the line
clearScreen();
graph.drawWave();
}
/**
* Called by the start method repeatedly. Draws the pixel array onto the JFrame using the
* BufferedImage magic.
*/
private void render() {
// Initialize buffer strategies
BufferStrategy bs = getBufferStrategy();
if (bs == null) {
createBufferStrategy(2);
return;
}
// Physically update the actual pixels on the image
Graphics g = (Graphics2D) bs.getDrawGraphics();
g.drawImage(image, 0, 0, getWidth(), getHeight(), null);
g.dispose();
bs.show();
}
/**
* Clears the screen by setting every pixel in the pixel array to black. Used to prevent
* ghost-images or blurring.
*/
public void clearScreen() {
for (int i = 0; i < pixels.length; i++)
pixels[i] = 0;
}
/**
* Main method to run the program. Creates a Screen object with a BufferedImage to display
* pixels however the other classes decide to. All this does is set up the JFrame with the
* proper parameters and properties to get it up and running.
*
* #param args A String array of random arguments that Java requires or it gets fussy
*/
public static void main(String[] args) {
// Create Screen object
Screen screen = new Screen();
screen.frame.add(screen);
screen.frame.pack();
screen.frame.setSize(WIDTH, HEIGHT);
screen.frame.setLocationRelativeTo(null);
screen.frame.setResizable(false);
screen.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
screen.frame.setVisible(true);
screen.start();
}
}
Second, my Keyboard class (KeyListener that breaks):
package com.elek.waves.input;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
/**
* Gets the user's key strokes and determines which keys are down at a given time.
*
* #author my name
* #version 1.0
*/
public class Keyboard implements KeyListener {
/**
* Holds the state of 120 keys (true if they're down, false if they're not).
*/
private boolean[] keys = new boolean[120];
/**
* Holds the state of the "useful" keys (true if down, false if not).
*/
public boolean w, a, s, d, up, down, left, right;
/**
* Determines if the "useful" keys are down or not. Sets the variables to true if they're down and
* false if they're up.
*/
public void update() {
w = keys[KeyEvent.VK_W];
a = keys[KeyEvent.VK_A];
s = keys[KeyEvent.VK_S];
d = keys[KeyEvent.VK_D];
up = keys[KeyEvent.VK_UP];
down = keys[KeyEvent.VK_DOWN];
left = keys[KeyEvent.VK_LEFT];
right = keys[KeyEvent.VK_RIGHT];
}
/**
* Changes the state of the pressed key's corresponding boolean in the array to true.
*/
public void keyPressed(KeyEvent e) {
keys[e.getKeyCode()] = true;
}
/**
* Changes the state of the pressed key's corresponding boolean in the array to false.
*/
public void keyReleased(KeyEvent e) {
keys[e.getKeyCode()] = false;
}
public void keyTyped(KeyEvent e) {
}
}
Third, my Controller class (uses the KeyListener to control the program):
package com.elek.waves.graphics.math;
import com.elek.waves.input.Keyboard;
/**
* Controls the graph's transformation properties (stretching and shifting). Directly changes the
* transformation variables in the Graph class to achieve this.
*
* #author my name
* #version 1.0
*/
public class Controller {
/**
* Keyboard object to get the user's key-inputs.
*/
private Keyboard input;
/**
* Graph object that this Controller will control.
*/
private Graph graph;
/* -- Constructor -- */
/**
* Create a new Controller object with the specific keyboard input parameter.
* <pre>Sets the starting parameters as the following:
* Vertical Scale: 1
* Horizontal Scale: 1
* Vertical Shift = 0
* Horizontal Shift = 0</pre>
*
* #param input The Keybaord object from which the controller will get input
*/
public Controller(Keyboard input, Graph parent) {
// Initialize keybaord input and graph parent
this.input = input;
graph = parent;
// Initialize transformation variables
graph.vScale = 50;
graph.hScale = 0.05;
graph.vShift = 0;
graph.hShift = 0;
}
/* -- Methods -- */
/**
* Updates the shifting of the graph (moving around) and the scaling of the graph (stretching)
* from the keyboard input. <strong>WASD</strong> keys control shifting, and <strong>up, down,
* left, and right</strong> keys control stretching.
*/
public void update() {
// Update shifting
if (input.w) graph.vShift += 0.5;
if (input.s) graph.vShift -= 0.5;
if (input.a) graph.hShift -= 0.04;
if (input.d) graph.hShift += 0.04;
// Update scaling
if (input.up) graph.vScale += 0.5;
if (input.down) graph.vScale -= 0.5;
if (input.left) graph.hScale += 0.0001;
if (input.right) graph.hScale -= 0.0001;
}
}
I have found several helpful people saying to use KeyBindings as opposed to a KeyListener. However, I have used a KeyListener successfully in the past, and I'd like to get it to work again if possible. If KeyBindings are absolutely necessary, I supposed I can make the switch, but I'd prefer if that didn't have to be the case.
Thank you all in advance!
Canvas will suffer the same issues that all the other components suffer from, loss of keyboard focus, this is why we generally don't recommend KeyListener.
First you need to make the Canvas focusable, see Canvas#setFocusable
The next, more difficult issue, is requesting keyboard focus, you can use Canvas#requestFocusInWindow but any component which requires keyboard focus will steal it.
Depending on what you are doing, you might be able to simply place the call in the update loop, but you need to be aware that if you want to ask input from the user, within the same window, you will have issues (with the canvas stealing the focus)
Update
I had some issues with index of bounds due to the use of an array in the keyboard controller, which I switched over to Set instead...
public class Keyboard implements KeyListener {
/**
* Holds the state of 120 keys (true if they're down, false if they're
* not).
*/
// private boolean[] keys = new boolean[120];
/**
* Holds the state of the "useful" keys (true if down, false if not).
*/
private Set<Integer> keys;
/**
* Determines if the "useful" keys are down or not. Sets the variables
* to true if they're down and false if they're up.
*/
public void update() {
keys = new HashSet<>(8);
}
public boolean isKeyPressed(int key) {
return keys.contains(key);
}
public boolean isWPressed() {
return isKeyPressed(KeyEvent.VK_W);
}
public boolean isAPressed() {
return isKeyPressed(KeyEvent.VK_A);
}
public boolean isSPressed() {
return isKeyPressed(KeyEvent.VK_S);
}
public boolean isDPressed() {
return isKeyPressed(KeyEvent.VK_D);
}
public boolean isUpPressed() {
return isKeyPressed(KeyEvent.VK_UP);
}
public boolean isDownPressed() {
return isKeyPressed(KeyEvent.VK_DOWN);
}
public boolean isLeftPressed() {
return isKeyPressed(KeyEvent.VK_LEFT);
}
public boolean isRightPressed() {
return isKeyPressed(KeyEvent.VK_RIGHT);
}
/**
* Changes the state of the pressed key's corresponding boolean in the
* array to true.
*/
public void keyPressed(KeyEvent e) {
System.out.println("Pressed = " + e.getKeyCode());
keys.add(e.getKeyCode());
}
/**
* Changes the state of the pressed key's corresponding boolean in the
* array to false.
*/
public void keyReleased(KeyEvent e) {
System.out.println("Released = " + e.getKeyCode());
keys.remove(e.getKeyCode());
}
public void keyTyped(KeyEvent e) {
}
}
I also added a small delay into the render loop so you're not chocking the system
private void start() {
setFocusable(true);
while (true) {
this.requestFocusInWindow();
update();
render();
try {
Thread.sleep(16);
} catch (InterruptedException ex) {
}
}
}
try this
import javax.swing.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
public class Main {
public static void main(String[] argv) throws Exception {
JTextField textField = new JTextField();
textField.addKeyListener(new MKeyListener());
JFrame jframe = new JFrame();
jframe.add(textField);
jframe.setSize(700, 700);
jframe.setVisible(true);
}
}
class MKeyListener extends KeyAdapter {
#Override
public void keyPressed(KeyEvent event) {
System.out.println(event.getKeyCode)
if(event.getKeyCode() = \\key code here\\){
System.out.println("True")
}else{System.out.println("False")
Allthough this was ran in package java but there shouldnt be anything wrong with it
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 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);
}
}
}
I have a JTextArea. I have a function that selects some amount of text when some combination is called. It's done properly. The thing is, I want to move caret to the selection beginning when some text is selected and VK_LEFT is pressed. KeyListener is implemented properly, I tested it in other way. The thing is, that when I write following code:
#Override public void keyPressed( KeyEvent e) {
if(e.getKeyChar()==KeyEvent.VK_LEFT)
if(mainarea.getSelectedText()!=null)
mainarea.setCaretPosition(mainarea.getSelectionStart());
}
and add an instance of this listener to mainarea, select some text (using my function) and press left arrow key, the caret position is set to the end of selection... And I wont it to be in the beginning... What's the matter? :S
Here's a code snippet
Action moveToSelectionStart = new AbstractAction("moveCaret") {
#Override
public void actionPerformed(ActionEvent e) {
int selectionStart = textComponent.getSelectionStart();
int selectionEnd = textComponent.getSelectionEnd();
if (selectionStart != selectionEnd) {
textComponent.setCaretPosition(selectionEnd);
textComponent.moveCaretPosition(selectionStart);
}
}
public boolean isEnabled() {
return textComponent.getSelectedText() != null;
}
};
Object actionMapKey = "caret-to-start";
textComponent.getInputMap().put(KeyStroke.getKeyStroke("LEFT"), actionMapKey);
textComponent.getActionMap().put(actionMapKey, moveToSelectionStart);
Note: it's not recommended to re-define typically installed keybindings like f.i. any of the arrow keys, users might get really annoyed ;-) Better look for some that's not already bound.
I have a JScrollPane with a JTextArea set as its view port.
I update the (multi line) text shown on the JTextArea continously about once a second. Each time the text updates, JScrollPane goes all the way to the bottom of the text.
Instead, I'd like to figure out the line number that is currently shown as the first line in the original text, and have that line be the first line shown when the text has been updated (or if the new text doesn't have that many lines, then scroll all the way to the bottom).
My first attempt of doing this was to get the current caret position, figure the line based on that, and then set the text area to show that line:
int currentPos = textArea.getCaretPosition();
int currentLine = 0;
try {
for(int i = 0; i < textArea.getLineCount(); i++) {
if((currentPos >= textArea.getLineStartOffset(i)) && (currentPos < gameStateTextArea.getLineEndOffset(i))) {
currentLine = i;
break;
}
}
} catch(Exception e) { }
textArea.setText(text);
int newLine = Math.min(currentLine, textArea.getLineCount());
int newOffset = 0;
try {
newOffset = textArea.getLineStartOffset(newLine);
} catch(Exception e) { }
textArea.setCaretPosition(newOffset);
This was almost acceptable for my needs, but requires the user to click inside the text area to change the caret position, so that the scrolling will maintain state (which isn't nice).
How would I do this using the (vertical) scroll position instead ?
I encountered the same problem and found that this answer includes a nice solution that works in this case:
DefaultCaret caret = (DefaultCaret) jTextArea.getCaret();
caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
This is pieced together, untested, from the API documentation:
use getViewport() on your JScrollPane to get a hold of the viewport.
use Viewport.getViewPosition() to get the top-left coordinates. These are absolute, not a percentage of scrolled text.
use Viewport.addChangeListener() to be notified when the top-left position changes (among other things). You may want to create a mechanism to distinguish user changes from changes your program makes, of course.
use Viewport.setViewPosition() to set the top-left position to where it was before the disturbance.
Update:
To stop JTextArea from scrolling, you may want to override its getScrollableTracksViewport{Height|Width}() methods to return false.
Update 2:
The following code does what you want. It's amazing how much trouble I had to go to to get it to work:
apparently the setViewPosition has to be postponed using invokeLater because if it's done too early the text update will come after it and nullify its effect.
also, for some weird reason perhaps having to do with concurrency, I had to pass the correct value to my Runnable class in its constructor. I had been using the "global" instance of orig and that kept setting my position to 0,0.
public class Sami extends JFrame implements ActionListener {
public static void main(String[] args) {
(new Sami()).setVisible(true);
}
private JTextArea textArea;
private JScrollPane scrollPane;
private JButton moreTextButton = new JButton("More text!");
private StringBuffer text = new StringBuffer("0 Silly random text.\n");
private Point orig = new Point(0, 0);
public Sami() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
getContentPane().setLayout(new BorderLayout());
this.textArea = new JTextArea() {
#Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
#Override
public boolean getScrollableTracksViewportWidth() {
return false;
}
};
this.scrollPane = new JScrollPane(this.textArea);
getContentPane().add(this.scrollPane, BorderLayout.CENTER);
this.moreTextButton.addActionListener(this);
getContentPane().add(this.moreTextButton, BorderLayout.SOUTH);
setSize(400, 300);
}
#Override
public void actionPerformed(ActionEvent arg0) {
int lineCount = this.text.toString().split("[\\r\\n]").length;
this.text.append(lineCount + "The quick brown fox jumped over the lazy dog.\n");
Point orig = this.scrollPane.getViewport().getViewPosition();
// System.out.println("Orig: " + orig);
this.textArea.setText(text.toString());
SwingUtilities.invokeLater(new LaterUpdater(orig));
}
class LaterUpdater implements Runnable {
private Point o;
public LaterUpdater(Point o) {
this.o = o;
}
public void run() {
// System.out.println("Set to: " + o);
Sami.this.scrollPane.getViewport().setViewPosition(o);
}
}
}