I have a JPopupMenu object. Its behavior depends on its coordinates.
How can I get its position relatively to its parent container?
In your MouseListener's method (mouseReleased etc.) you should receive a MouseEvent object containing the current position. If you don't want to use those values you can try using the Component#getLocation method, otherwise the Component#getLocationOnScreen but it returns the absolute position, then you need to calculate the relative one.
There is a solution, but it is not recommended, because if there is a SecurityManager it may fail (force the field to be accessible):
public static Container getTopParent(#Nonnull Component c) {
Container lastNotNull = (Container) c;
Container p = c.getParent();
if (p != null)
lastNotNull = p;
while(p != null) {
lastNotNull = p;
p = p.getParent();
}
return lastNotNull;
}
public static int getClickedXThatInvokedPopup(#Nonnull ActionEvent ev) {
try {
JPopupMenu topParent = (JPopupMenu) getTopParent((Component) ev.getSource());
java.lang.reflect.Field fieldX = topParent.getClass().getDeclaredField("desiredLocationX");
fieldX.setAccessible(true);
int x = (Integer) fieldX.get(topParent);
Point p = new Point(x, 0);
SwingUtilities.convertPointFromScreen(p, topParent.getInvoker());
return p.x;
} catch(NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
System.err.println("Cannot get clicked point: " + ex);
return -1;
}
}
Since neither the Component#getLocation nor the Component#getLocationOnScreen method worked for me, and the fields desiredLocationX/desiredLocationY are not accessible, I extended JPopupMenu() as follows:
contextMenu = new JPopupMenu(){
private Point desiredLocation;
/**
* Override Component#getLocation, since it always returns 0,0.
*/
#Override
public Point getLocation() {
return desiredLocation;
}
#Override
public void show(Component invoker, int x, int y) {
desiredLocation = new Point(x, y);
super.show(invoker, x, y);
}
};
Related
In JavaFX's TableView (and TreeTableView) it's really hard to reorder columns using drag & drop when the horizontal scrollbar is present, because the table doesn't scroll automatically when one want's to drag the column to the currently not visible (off the scroll pane viewport) position.
I've noticed that there are already a bug (enhancement) reports for this:
https://bugs.openjdk.java.net/browse/JDK-8092314
https://bugs.openjdk.java.net/browse/JDK-8092355
https://bugs.openjdk.java.net/browse/JDK-8213739
... but as it haven't been tackled for quite some time I am wondering whether there is any other way to achieve the same behavior using the current API.
There is SSCCE:
public class TableViewColumnReorderDragSSCCE extends Application {
public static final int NUMBER_OF_COLUMNS = 30;
public static final int MAX_WINDOW_WIDTH = 480;
#Override
public void start(Stage stage) {
stage.setScene(new Scene(createTable()));
stage.show();
stage.setMaxWidth(MAX_WINDOW_WIDTH);
}
private TableView<List<String>> createTable() {
final TableView<List<String>> tableView = new TableView<>();
initColumns(tableView);
return tableView;
}
private void initColumns(TableView<List<String>> tableView) {
for (int i=0; i<NUMBER_OF_COLUMNS; i++) {
tableView.getColumns().add(new TableColumn<>("Column " + i));
}
tableView.getItems().add(Collections.emptyList());
}
}
Steps to reproduce:
Run the above SSCCE
Try to drag Column 0 after Column 29
I am after a fully functional solution (if any).
As no complete solution was provided I've came up with one of my own. I've introduced a (ColumnsOrderingEnhancer) implementation which will enhance the table view columns reordering by automatic scrolling (when needed).
Usage (with the table view defined in the above SSCCE):
// Enhance table view columns reordering
final ColumnsOrderingEnhancer<List<String>> columnsOrderingEnhancer = new ColumnsOrderingEnhancer<>(tableView);
columnsOrderingEnhancer.init();
ColumnsOrderingEnhancer implementation:
public class ColumnsOrderingEnhancer<T> {
private final TableView<T> tableView;
public ColumnsOrderingEnhancer(TableView<T> tableView) {
this.tableView = tableView;
}
public void init() {
tableView.skinProperty().addListener((observable, oldSkin, newSkin) -> {
// This can be done only when skin is ready
final TableHeaderRow header = (TableHeaderRow) tableView.lookup("TableHeaderRow");
final MouseDraggingDirectionHelper mouseDraggingDirectionHelper = new MouseDraggingDirectionHelper(header);
final ScrollBar horizontalScrollBar = getTableViewHorizontalScrollbar();
// This is the most important part which is responsible for scrolling table during the column dragging out of the viewport.
header.addEventFilter(MouseEvent.MOUSE_DRAGGED, event -> {
final double totalHeaderWidth = header.getWidth();
final double xMousePosition = event.getX();
final MouseDraggingDirectionHelper.Direction direction = mouseDraggingDirectionHelper.getLastDirection();
maybeChangeScrollBarPosition(horizontalScrollBar, totalHeaderWidth, xMousePosition, direction);
});
});
}
private void maybeChangeScrollBarPosition(ScrollBar horizontalScrollBar, double totalHeaderWidth, double xMousePosition, MouseDraggingDirectionHelper.Direction direction) {
if (xMousePosition > totalHeaderWidth && direction == RIGHT) {
horizontalScrollBar.increment();
}
else if (xMousePosition < 0 && direction == LEFT) {
horizontalScrollBar.decrement();
}
}
private ScrollBar getTableViewHorizontalScrollbar() {
Set<Node> scrollBars = tableView.lookupAll(".scroll-bar");
final Optional<Node> horizontalScrollBar =
scrollBars.stream().filter(node -> ((ScrollBar) node).getOrientation().equals(Orientation.HORIZONTAL)).findAny();
try {
return (ScrollBar) horizontalScrollBar.get();
}
catch (NoSuchElementException e) {
return null;
}
}
/**
* A simple class responsible for determining horizontal direction of the mouse during dragging phase.
*/
static class MouseDraggingDirectionHelper {
private double xLastMousePosition = -1;
private Direction direction = null;
MouseDraggingDirectionHelper(Node node) {
// Event filters that are determining when scrollbar needs to be incremented/decremented
node.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> xLastMousePosition = event.getX());
node.addEventFilter(MouseEvent.MOUSE_DRAGGED, event -> {
direction = ((event.getX() - xLastMousePosition > 0) ? RIGHT : LEFT);
xLastMousePosition = event.getX();
});
}
enum Direction {
LEFT,
RIGHT
}
public Direction getLastDirection() {
return direction;
}
}
}
End result (which works surprisingly well):
It's not impossible to work around. You could start with something like this, though it is a very crude implementation, I'm sure in principle it can be refined to something reasonable:
tableView.setOnMouseExited(me -> {
if (me.isPrimaryButtonDown()) { // must be dragging
Bounds tvBounds = tableView.getBoundsInLocal();
double x = me.getX();
if (x < tvBounds.getMinX()) {
// Scroll to the left
tableView.scrollToColumnIndex(0);
} else if (x > tvBounds.getMaxX()) {
// Scroll to the right
tableView.scrollToColumnIndex(tableView.getColumns().size()-1);
}
}
});
In a proper implementation you would likely have to sneak around the Node hierarchy and find the width of the table columns and determine what the next out-of-view column is so you can scroll to the exact right column. Remember when you did that so you can do it again if the user continues to drag outside the table, but not too fast.
EDIT: Based on your self-answer, here is my take on it. I've compacted your code a bit and made it work on JavaFX 8.0:
static class TableHeaderScroller implements EventHandler<MouseEvent> {
private TableView tv;
private Pane header;
private ScrollBar scrollBar;
private double lastX;
public static void install(TableView tv) {
TableHeaderScroller ths = new TableHeaderScroller(tv);
tv.skinProperty().addListener(ths::skinListener);
}
private TableHeaderScroller(TableView tv) {
this.tv = tv;
}
private void skinListener(ObservableValue<? extends Skin<?>> observable, Skin<?> oldValue, Skin<?> newValue) {
if (header != null) {
header.removeEventFilter(MouseEvent.MOUSE_DRAGGED, this);
}
header = (Pane) tv.lookup("TableHeaderRow");
if (header != null) {
tv.lookupAll(".scroll-bar").stream().map(ScrollBar.class::cast)
.filter(sb -> sb.getOrientation() == Orientation.HORIZONTAL)
.findFirst().ifPresent( sb -> {
TableHeaderScroller.this.scrollBar = sb;
header.addEventFilter(MouseEvent.MOUSE_DRAGGED, TableHeaderScroller.this);
});
}
}
#Override
public void handle(MouseEvent event) {
double x = event.getX();
double sign = Math.signum(x - lastX);
lastX = x;
int dir = x < 0 ? -1 : x > header.getWidth() ? 1 : 0;
if (dir != 0 && dir == sign) {
if (dir < 0) {
scrollBar.decrement();
} else {
scrollBar.increment();
}
}
}
}
I'm doing a school project for a chess game and I'm currently stuck at the DnD operation of the pieces.
In the code, I passed the TransferHandler.MOVE parameter in exportAsDrag() to make it a MOVE operation. However, the behavior of the TransferHandler is still COPY instead of MOVE, when dragging and dropping icon from JLabels.
I tried setting the icon of the source JLabel to null in exportDone() in TransferHandler anonymous class but the icon will disappear if the source and destination of the DnD operation is the same. If there is any more methods I should override/add or any other way to accomplish the same thing, please let me know about it.
MouseListener listener = new MouseAdapter()
{
#Override
public void mousePressed(MouseEvent e)
{
ChessTiles c = (ChessTiles) e.getSource();
TransferHandler handler = c.getTransferHandler();
handler.exportAsDrag(c, e, TransferHandler.MOVE)
}
};
private static TransferHandler handler = new TransferHandler("icon")
{
#Override
public int getSourceActions (JComponent c)
{
return MOVE;
}
};
tileArray[x][y].addMouseListener(listener);
tileArray[x][y].setTransferHandler(handler);
Consider the documentation of TransferHandler.exportDone:
Invoked after data has been exported. This method should remove the data that was transferred if the action was MOVE.
This should answer both question. First, you are indeed responsible to implement the move semantic and second, you should only do it when the action has the value MOVE. Besides the possibility of other transfer types, which doesn’t apply to your scenario, as you do not support them, it may get invoked with a zero action, to allow cleanup after an aborted transfer. This may even happen right from the exportAsDrag method when the preconditions were not met.
If you do not want to support dragging onto itself, you may disable the drop target temporarily, using the exportDone method for resetting the property.
E.g.
public class DragAndDropExample {
public static void main(String[] args) {
EventQueue.invokeLater(DragAndDropExample::init);
}
private static void init() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch(ReflectiveOperationException|UnsupportedLookAndFeelException ex) {}
try {
BufferedImage img = ImageIO.read(
new URL("https://cdn.sstatic.net/img/favicons-sprite32.png"));
img = img.getSubimage(0, 11844, 32, 32);
ICON = new ImageIcon(img);
} catch(IOException ex) {
ICON = UIManager.getIcon("OptionPane.errorIcon");
}
JFrame frame = new JFrame("Test");
Container c = frame.getContentPane();
final int gridWidth = 4, gridHeight = 4;
c.setLayout(new GridLayout(gridHeight, gridWidth, 4, 4));
for(int y = 0; y < gridHeight; y++) {
for(int x = 0; x < gridWidth; x++) {
create(x, y, c);
}
}
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
static Icon ICON;
static final MouseAdapter DRAG_INIT = new MouseAdapter() {
#Override public void mousePressed(MouseEvent e) {
var c = (JComponent) e.getSource();
var handler = c.getTransferHandler();
handler.exportAsDrag(c, e, TransferHandler.MOVE);
}
};
static final TransferHandler ICON_TRANSFER = new TransferHandler( "icon" ) {
#Override public void exportAsDrag(JComponent comp, InputEvent e, int action) {
comp.getDropTarget().setActive(false);
super.exportAsDrag(comp, e, action);
}
#Override public int getSourceActions(JComponent c) {
return MOVE;
}
#Override protected void exportDone(
JComponent source, Transferable data, int action) {
source.getDropTarget().setActive(true);
if (action == MOVE) {
((JLabel)source).setIcon(null);
}
}
};
private static void create(int x, int y, Container c) {
JLabel l = new JLabel("\u00a0");
if(x == 0 && y == 0) l.setIcon(ICON);
l.setBorder(BorderFactory.createLineBorder(Color.lightGray, 1));
l.setTransferHandler(ICON_TRANSFER);
l.addMouseListener(DRAG_INIT);
c.add(l);
}
}
If you do not want to disable it, you may store the component, to check whether source and target are the same like in this answer, but you should set the remembered component to null in the exportDone method, to ensure that there are no memory leaks.
I tried setting the icon of the source JLabel to null in exportDone() in TransferHandler anonymous class but the icon will disappear if the source and destination of the DnD operation is the same.
Yes, that needs to be done.
Also, by overriding the importData(...) method you can save the "target" component so you can check if the source/target are the same component:
TransferHandler iconHandler = new TransferHandler( "icon" )
{
Component target;
#Override
public int getSourceActions(JComponent c)
{
return MOVE;
}
#Override
public boolean importData(TransferSupport info)
{
target = info.getComponent();
return super.importData( info );
}
#Override
protected void exportDone(JComponent source, Transferable data, int action)
{
if (action == MOVE
&& source != target)
{
((JLabel)source).setIcon(null);
}
}
};
Someone suggested that I override the exportDone(...) function of TransferHandler like so,
#Override
protected void exportDone(JComponent source, Transferable data, int action)
{
if (action == MOVE)
{
((JLabel)source).setIcon(null);
}
//((JLabel)source).setIcon(null);
}
Without the if the statement, if I set the icon to null, the icon will disappear, regardless of the bool returned by importData(...). With it, the icon stays if the returned value of importData(...) is false. So is it safe to assume that after importData(...) is called, then only exportDone(...) is called?
It works but now I'm curious about the sequence of TransferHandler's internal function calls, after handler.exportAsDrag(...) is invoked.
I would like to make the point info tooltip appear faster. How can i do it? with the default setting I have to hover the mouse onto the point, then wait to be able to see point coordinate information. I want the point coordinates to be immediately available. How can i do that?
ChartPanel provides getInitialDelay() and setInitialDelay() to query and alter "the initial tooltip delay value used inside this chart panel." As a concrete example based on BarChartDemo1, the following change to the constructor eliminates the initial delay entirely:
public BarChartDemo1(String title) {
super(title);
…
chartPanel.setInitialDelay(0);
setContentPane(chartPanel);
}
It's a late solution but here it is.
Here is how i handled with JavaFX.
It shows the tooltip fast instant and tooltip does not fade after a while.
/**
* The "tooltip" is the hard-coded id for the tooltip object.
* It's set inside the JFreeChart Lib.
* */
public static String TOOLTIP_ID = "tooltip";
public static void removeTooltipHandler(ChartViewer chartViewer) {
chartViewer.getCanvas().removeAuxiliaryMouseHandler(chartViewer.getCanvas().getMouseHandler(TOOLTIP_ID));
}
public static void addFasterTooltipHandler(ChartViewer chartViewer) {
if(chartViewer.getCanvas().getMouseHandler(TOOLTIP_ID) != null) {
removeTooltipHandler(chartViewer);
}
chartViewer.getCanvas().addAuxiliaryMouseHandler(new TooltipHandlerFX(TOOLTIP_ID) {
Tooltip tooltip;
boolean isVisible = false;
#Override
public void handleMouseMoved(ChartCanvas canvas, MouseEvent e) {
if (!canvas.isTooltipEnabled()) {
return;
}
String text = getTooltipText(canvas, e.getX(), e.getY());
setTooltip(canvas, text, e.getScreenX(), e.getScreenY());
}
private String getTooltipText(ChartCanvas canvas, double x, double y) {
ChartRenderingInfo info = canvas.getRenderingInfo();
if (info == null) {
return null;
}
EntityCollection entities = info.getEntityCollection();
if (entities == null) {
return null;
}
ChartEntity entity = entities.getEntity(x, y);
if (entity == null) {
return null;
}
return entity.getToolTipText();
}
// This function is copied from Canvas.setTooltip and manipulated as needed.
public void setTooltip(ChartCanvas canvas, String text, double x, double y) {
if (text != null) {
if (this.tooltip == null) {
this.tooltip = new Tooltip(text);
this.tooltip.autoHideProperty().set(false); // Disable auto hide.
Tooltip.install(canvas, this.tooltip);
} else {
this.tooltip.setText(text);
this.tooltip.setAnchorX(x);
this.tooltip.setAnchorY(y);
}
this.tooltip.show(canvas, x, y);
isVisible = true;
} else {
if(isVisible) {
this.tooltip.hide();
isVisible = false;
}
}
}
});
}
I have wee bit of code that updates a JSlider; and at another point in time, the JSlider's maximum value needs to updated. The problem is when I call setMaximum() on the slider, it also dispatches a ChangeEvent. To avoid that I'm doing this:
slider.removeChangeListener(this);
slider.setMaximum(newMax);
slider.addChangeListener(this);
Is there a cleaner/more elegant way of doing this?
A clean way might be (depending a bit on what you actually need) to implement a custom BoundedRangeModel which fires a custom ChangeEvent that can carry the actually changed properties:
/**
* Extended model that passes the list of actually changed properties
* in an extended changeEvent.
*/
public static class MyBoundedRangeModel extends DefaultBoundedRangeModel {
public MyBoundedRangeModel() {
}
public MyBoundedRangeModel(int value, int extent, int min, int max) {
super(value, extent, min, max);
}
#Override
public void setRangeProperties(int newValue, int newExtent, int newMin,
int newMax, boolean adjusting) {
int oldMax = getMaximum();
int oldMin = getMinimum();
int oldValue = getValue();
int oldExtent = getExtent();
boolean oldAdjusting = getValueIsAdjusting();
// todo: enforce constraints of new values for all
List<String> changedProperties = new ArrayList<>();
if (oldMax != newMax) {
changedProperties.add("maximum");
}
if (oldValue != newValue) {
changedProperties.add("value");
}
// todo: check and add other properties
changeEvent = changedProperties.size() > 0 ?
new MyChangeEvent(this, changedProperties) : null;
super.setRangeProperties(newValue, newExtent, newMin, newMax, adjusting);
}
}
/**
* Extended ChangeEvent that provides a list of actually
* changed properties.
*/
public static class MyChangeEvent extends ChangeEvent {
private List<String> changedProperties;
/**
* #param source
*/
public MyChangeEvent(Object source, List<String> changedProperties) {
super(source);
this.changedProperties = changedProperties;
}
public List<String> getChangedProperties() {
return changedProperties;
}
}
Its usage something like:
final JSlider slider = new JSlider();
slider.setModel(new MyBoundedRangeModel(0, 0, -100, 100));
ChangeListener l = new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
if (e instanceof MyChangeEvent) {
MyChangeEvent me = (MyChangeEvent) e;
if (me.getChangedProperties().contains("value")) {
System.out.println("new value: " +
((BoundedRangeModel) e.getSource()).getValue());
}
if (me.getChangedProperties().contains("maximum")) {
System.out.println("new max: " +
((BoundedRangeModel) e.getSource()).getMaximum());
}
} else {
// do something else or nothing
}
}
};
slider.getModel().addChangeListener(l);
Note that you have to register the listener with the model, not with the slider (reason being that the slider creates a new changeEvent of the plain type)
You could check who triggered the change in the listener. It's still pretty dirty but you won't have to remove the change listener.
It looks like the same problem, in essence, as this question. In which case, I fear the answer is "no"
if you just need your slider to fire events when value is changing you can simplify kleopatra's answer in the following way:
// make sure slider only fires changEvents when value changes
slider.setModel(new DefaultBoundedRangeModel() {
final ChangeEvent theOne=new ChangeEvent(this);
#Override
public void setRangeProperties(int newValue, int newExtent, int newMin,int newMax, boolean adjusting)
changeEvent= (getValue() != newValue ? theOne:null);
super.setRangeProperties(newValue, newExtent, newMin, newMax, adjusting);
}
#Override
protected void fireStateChanged()
{
if(changeEvent==null) return;
super.fireStateChanged();
}
});
Then all you need to do is register a "Standard" ChangeListener on the model and it will only get called when the value is changing not when maximum or minimum changes.
slider.getModel().addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
// do something here when value changes
});
});
I would like to know how can I extend JEditorPane (or any other swing text editing component) to handle rectangle (column) selection mode. It is a well known feature in current text editors where you can select multiple lines (rows) starting from an offset (column) and ending by an offset (column) which look like selecting a rectangle of text, and then what you type will overwrite the selection in each line (row) concurrently.
One idea was to override the selection and create fake selection by highlighting each line in rectangle form by following the mouse events, and keeping track of such information to use it when typing. However, I am not quit sure how to override the selection and track the mouse, nor how to redirect typing to affect each line.
Any help in any form would be appreciated.
Found out this little code snippet, that involves a Custom Caret (to handle the fragmented selection) and Highlighter (to show the fragments):
class MyCaret extends DefaultCaret {
Point lastPoint=new Point(0,0);
public void mouseMoved(MouseEvent e) {
super.mouseMoved(e);
lastPoint=new Point(e.getX(),e.getY());
}
public void mouseClicked(MouseEvent e) {
super.mouseClicked(e);
getComponent().getHighlighter().removeAllHighlights();
}
protected void moveCaret(MouseEvent e) {
Point pt = new Point(e.getX(), e.getY());
Position.Bias[] biasRet = new Position.Bias[1];
int pos = getComponent().getUI().viewToModel(getComponent(), pt, biasRet);
if(biasRet[0] == null)
biasRet[0] = Position.Bias.Forward;
if (pos >= 0) {
setDot(pos);
Point start=new Point(Math.min(lastPoint.x,pt.x),Math.min(lastPoint.y,pt.y));
Point end=new Point(Math.max(lastPoint.x,pt.x),Math.max(lastPoint.y,pt.y));
customHighlight(start,end);
}
}
protected void customHighlight(Point start, Point end) {
getComponent().getHighlighter().removeAllHighlights();
int y=start.y;
int firstX=start.x;
int lastX=end.x;
int pos1 = getComponent().getUI().viewToModel(getComponent(), new Point(firstX,y));
int pos2 = getComponent().getUI().viewToModel(getComponent(), new Point(lastX,y));
try {
getComponent().getHighlighter().addHighlight(pos1,pos2,
((DefaultHighlighter)getComponent().getHighlighter()).DefaultPainter);
}
catch (Exception ex) {
ex.printStackTrace();
}
y++;
while (y<end.y) {
int pos1new = getComponent().getUI().viewToModel(getComponent(), new Point(firstX,y));
int pos2new = getComponent().getUI().viewToModel(getComponent(), new Point(lastX,y));
if (pos1!=pos1new) {
pos1=pos1new;
pos2=pos2new;
try {
getComponent().getHighlighter().addHighlight(pos1,pos2,
((DefaultHighlighter)getComponent().getHighlighter()).DefaultPainter);
}
catch (Exception ex) {
ex.printStackTrace();
}
}
y++;
}
}
}
Anyway, I've never run that code (it's Stanislav's).