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).
Related
I'm trying to catch the event when the small "minimize" or "maximize" arrows of a JSplitPane's divider are clicked.
I found a way to listen to a click elsewhere on the divider bar. Something like :
SplitPaneUI spui = splitPane.getUI();
if (spui instanceof BasicSplitPaneUI) {
((BasicSplitPaneUI) spui).getDivider().addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
// do something...
}
});
}
But is there a way to listen to clicks on the arrows?
I'm trying to catch the event when the small "minimize" or "maximize" arrows of a JSplitPane's divider are clicked.
Maybe you could add a PropertyChangeListener to the JSPlitPane and listen for dividerLocation changes (assuming you don't care if the divider was dragged or "one clicked" to the start or end). Then you can check if the divider location is at 0 or the maximum.
splitPane.addPropertyChangeListener("dividerLocation", this);
...
public void propertyChange(PropertyChangeEvent e)
{
// Get the new divider location of the split pane
int location = ((Integer)e.getNewValue()).intValue();
if (location == 0)
// do something
else if (location == splitPane.getMaximumDividerLocation())
// do something else
}
If you do care about the difference between dragging and clicking, then maybe you can compare the old/new values and look for a change in the location greater than a specified value.
Another option is to get the button from the divider:
JSplitPane splitPane = (JSplitPane)e.getSource();
BasicSplitPaneUI ui = (BasicSplitPaneUI)splitPane.getUI();
BasicSplitPaneDivider divider = ui.getDivider();
JButton button1 = (JButton)divider.getComponent(0)
Now you can add another ActionListener to the button to do your custom code.
Reflection is the Only way I guess to listen to One Touch Expandable Buttons
Just pass addSplitPaneListener and it is done.
public void addSplitPaneListener(JSplitPane splitPane) {
addSplitPaneListener("leftButton", splitPane);
addSplitPaneListener("rightButton", splitPane);
}
public void addSplitPaneListener(String button, JSplitPane splitPane) {
try {
Field field = BasicSplitPaneDivider.class.getDeclaredField(button);
field.setAccessible(true);
JButton onetouchButton = (JButton) field.get(((BasicSplitPaneUI) splitPane.getUI()).getDivider());
onetouchButton.setActionCommand(button);
onetouchButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
buttonClicked(e);
}
});
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
//Logger.getLogger(NewJFrame1.class.getName()).log(Level.SEVERE, null, ex);
}
}
public void buttonClicked(ActionEvent e) {
System.out.println("Clicked " + e.getActionCommand());
}
Here is a derived class that deals with expanding/hiding either using the one touch expander and/or an additional keylistener bound to a certain key to hide/unhide the RIGHT panel. Doing it for the left should easy to adapt ;)
Interestingly on Linux the right part will never get its theoretical maximum value but will be one less (BUGCLICKONETOUCH). Didn't test if this is also true on Windows.
import java.awt.Component;
import javax.swing.JSplitPane;
public class JToggleSplitPane extends JSplitPane {
private int restoreWidth = -1;
private final static int BUGCLICKONETOUCH = 1;
public JToggleSplitPane(int splittype, Component c1, Component c2) {
super(splittype, c1, c2);
setOneTouchExpandable(true);
}
public void toggle() {
try {
if (getDividerLocation() < getWidth() - getDividerSize() - BUGCLICKONETOUCH) {
restoreWidth = getWidth() - getDividerSize() - getDividerLocation();
setDividerLocation(1.0d);
} else {
setDividerLocation(getWidth() - getDividerSize() - restoreWidth);
}
updateUI();
doLayout();
} catch (Exception e) {
e.printStackTrace();
}
}
public void doLayout() {
super.doLayout();
if (restoreWidth == -1) {
restoreWidth = rightComponent.getWidth();
}
}
}
I'm trying to design a game like BeJeweled using java language
This is where I've reached so far :
public class Game {
public static void main(String[] args) throws InterruptedException {
final JFrame window = new JFrame();
window.setSize(508,669);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final Grid g=new Grid();
Game.obj(g);
window.add(g);
window.setVisible(true);
window.setResizable(false);
window.repaint(2);
window.addMouseListener(new MouseListener(){
#Override
public void mouseClicked(MouseEvent e) {
int x=e.getX();
int y=e.getY();
for(int i=0;i<10;i++) {
for(int j=0;j<16;j++){
if(x>i*50+5 && x<i*50+54 && y>j*40+26 && y<j*40+26+39){
g.b[i][j]=g.a[i][j];
int q = x;
int w=y;
int r =x;
int t =y;
q-=50;
w-=40;
if( i>0&&g.a[i-1][j]==g.b[i][j]){
g.a[i][j]=0;
g.a[i-1][j]=0;
}
if( j>0&&g.a[i][j-1]==g.b[i][j]){
g.a[i][j]=0;
g.a[i][j-1]=0;
}
r+=50;
t+=40;
if(i<9&&g.a[i+1][j]==g.b[i][j]){
g.a[i][j]=0;
g.a[i+1][j]=0;
}
if(j<15&&g.a[i][j+1]==g.b[i][j]){
g.a[i][j]=0;
g.a[i][j+1]=0;
}
}
}
}
SwingUtilities.updateComponentTreeUI(window);
}
#Override
public void mousePressed(MouseEvent e) {
}
#Override
public void mouseReleased(MouseEvent e) {
}
#Override
public void mouseEntered(MouseEvent e) {
}
#Override
public void mouseExited(MouseEvent e) {
}
});
}
public static void obj(Grid g){
Random r =new Random();
for(int k=0;k<10;k++)
for(int l=0;l<16;l++)
g.a[k][l]=1+r.nextInt(4);
}
}
class Grid extends JPanel {
private Graphics Graphics;
#Override
public void paint(Graphics g) {
final Graphics g1 = g ;
this.setGraphics(g);
for(int i=0;i<600;i+=50)
for(int j=0;j<400;j+=40)
g.drawRect(i, j, i+50, j+40);
for(int i=0;i<10;i++)
for(int j=0;j<16;j++){
if(a[i][j]== 0) g.setColor(Color.WHITE);
if(a[i][j] == 1) g.setColor(Color.ORANGE);
if(a[i][j] == 2) g.setColor(Color.red);
if(a[i][j] == 3) g.setColor(Color.GREEN);
if(a[i][j] == 4) g.setColor(Color.cyan);
g.fillRect(i*50+1, j*40+1, 49, 39);
}
// Mouselis mouseptr = new Mouselis(g);
// this.addMouseListener(mouseptr);
// this.addMouseMotionListener(mouseptr);
}
public void setGraphics(Graphics Graphics) {
this.Graphics = Graphics;
}
int [][] a= new int[10][16];
int [][] b= new int[10][16];
}
In this design only the up down left right of rectangles are being checked for same color in the method mouse clicked. How can i make it check all the near rectangles for the one having same color ?
Please help Thanks
You have four if statements that check the adjacent rectangles for the same color as the one that was clicked. Here is the last one:
if(j<15&&g.a[i][j+1]==g.b[i][j]){
g.a[i][j]=0;
g.a[i][j+1]=0;
}
j, the vertical grid coordinate, is compared with 15 to be sure that j+1 is within bounds. Also, a rectangle in the 2d-array a is checked to see if it is equal to the rectangle in the 2d-array b that got clicked. If it is, you set it to 0 for Color.WHITE. Because the indices are i and j+1 this checks the rectangle directly below the clicked one.
To check a rectangle that is diagonally adjacent, change both indices by one.
The rectangle to the bottom right is g.a[i+1][j+1]. To access this rectangle without an error you need to be sure that both i+1 and j+1 are within the bounds of the array to avoid an error.
So, the if statement is
if(j<15 && i < 9 && g.a[i+1][j+1]==g.b[i][j]){ ...
You can figure out the rest.
Java is a strong Object-Oriented programming language - especially before Java8: I think that you should really work with more Objects that would hold their own responsibilities and behaviours.
Once you'll have a class that represents a Jewel, just implement some recursive method to know if it's part of some "Jewel streak". For example:
public class Jewel {
private final JewelType type; // Could be an Enum for example
// Many other stuff to add (like a constructor at least!)
public Collection<Jewel> getStreak() {
final Set<Jewel> res = new HashSet<>();
this._getStreak(res);
return res;
}
private void _getStreak(final Set<Jewel> streak) {
// Just leave the method if we've already visited this Jewel
if(!streak.add(this)) {
return;
}
// Assuming that this method returns a Collection of the Jewels that are stored West, North, South and East from this
for(final Jewel neighbour : Grid.getInstance().getJewelsAround(this)) {
if(neighbour.getJewelType().equals(this.getJewelType())) {
neighbour._getStreak(streak);
}
}
}
}
When trying to see if some Jewel is part of a streak that is long enough to be "destroyed", just check something like this:
Jewel b = Grid.getInstance().getJewelForCoordinates(x, y);
final Collection<Jewel> streak = b.getStreak();
if(streak.size() >= Game.MINIMUM_STREAK_SIZE) {
Grid.getInstance().destroyJewels(streak);
}
I just don't even have access to any Java editor/compiler right now so please excuse me, would my code present any mistyping. Even though I recommend to do many changes, I hope it can help you :)
By the way, if you don't want to override all the methods described by the MouseListener interface, use the following:
window.addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(final MouseEvent event) {
// Code to be executed on mouseClicked().
}
});
MouseAdapter is an abstract class that implements MouseListener and does nothing when receiving any event. You just have to override the methods that handle the events you're interested in ;)
another problem. I wanted to make doubleclick on JTable which open new window with form. So finally i made it this way:
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.getSelectionModel().addListSelectionListener(new ListSelectionListener(){
public void valueChanged(ListSelectionEvent event){
int viewRow = table.getSelectedRow();
if(viewRow < 0)
System.out.println("LOL");
else{
final int modelRow = table.convertRowIndexToModel(viewRow);
table.addMouseListener(new MouseAdapter(){
public void mouseClicked(MouseEvent e){
if(e.getClickCount() == 2)
try {
new BookForm();
} catch (IOException e1) {
e1.printStackTrace();
}
}
});
}
}
});
It works, but not perfect. First time when i doubleclick on JTable it opens 2 windows (why not one?), next time it opens 4 windows, next another 6 windows, etc. Any ideas? Maybe i should have to use different method? Thanks for help!
Take a second to look over your code...
Each time the selection changes, you add a new MouseListener
table.getSelectionModel().addListSelectionListener(new ListSelectionListener(){
public void valueChanged(ListSelectionEvent event){
int viewRow = table.getSelectedRow();
if(viewRow < 0)
System.out.println("LOL");
else{
// You add a new mouse listener...
final int modelRow = table.convertRowIndexToModel(viewRow);
table.addMouseListener(new MouseAdapter(){
public void mouseClicked(MouseEvent e){
if(e.getClickCount() == 2)
try {
new BookForm();
} catch (IOException e1) {
e1.printStackTrace();
}
}
});
}
}
});
So, when you "finally" double click a row, you will have 1-n MouseListeners registered against the table...
You could just get rid of the selection listener and simple add the MouseListener directly to the table...
table.addMouseListener(new MouseAdapter(){
public void mouseClicked(MouseEvent e){
if(e.getClickCount() == 2)
int selectedRow = table.getSelectedRow();
if (selectedRow > -1) {
int modelRow = table.convertRowIndexToModel(selectedRow);
try {
new BookForm();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
});
Also take a look at The Use of Multiple JFrames: Good or Bad Practice? before you bomb bard your user with lots of new windows...
I am searching for a way to create a touch application in Java (NOT for mobile devices) with a touch scroll support.
Ive been searching so far and I am investigating how this could be done - what ive found is the MT4J (http://www.mt4j.org/) but it seems that it would not support that (please correct me if i am wrong).
So my question is, how can i emulate a scroll event on a horizontal touch / swipe?
Thanks for help!
Kind Regards,
Alex
This implements a touch and drag scroller by sub-classing JScrollPane.
Because touch and drag is not enough on its own I have added momentum
so that it 'throws' the scroll when the mouse button is released.
There is no 'bounce' at the ends of the scroll as I cannot afford to
fight a lawsuit against the owners of 'bounce'.
It's not completely encapsulated because, although it works fine if
the view is a jlist, there may be components on the view that need
to modify their response if the panel was being dragged at the time.
Also some components, e.g. JRadioButton, JCheckBox, etc, consume
mouse clicks without passing them up to the container, so you will
need to add TouchScroll's MouseListener and MouseMotionListener to them.
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
public class TouchScroll extends JScrollPane implements MouseListener, MouseMotionListener {
private JScrollBar vbar = this.getVerticalScrollBar();
public TouchScroll(Component view){ // 1-arity CONSTRUCTOR
super(view);
view.addMouseListener(this);
view.addMouseMotionListener(this);
}
public TouchScroll() { super(); } // 0-arity CONSTRUCTOR
public void setViewportView(Component view) {
super.setViewportView(view);
view.addMouseListener(this);
view.addMouseMotionListener(this);
}
private static boolean wasdragged = false; // other MouseListeners may need to know this ...
public boolean wasDragged() { return wasdragged; } // ... this gives them safe access
static int lastY = 0, distY = 0;
double momentum = 0; // not really physical momentum but it will be used to 'throw' the scroll when the mouse button is released
static boolean lbdown = false;
#Override
public void mouseDragged(MouseEvent e) {
wasdragged = true;
distY = 0;
int currentY = e.getYOnScreen();
if(lbdown) {
distY = lastY - currentY;
vbar.setValue(distY + vbar.getValue());
if(Math.abs(distY) > 1)
momentum = distY + distY; // magnify and log the momentum for later use
}
lastY = currentY;
}
#Override
public void mousePressed(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
lastY = e.getYOnScreen();
lbdown = true;
}
}
#Override
public void mouseReleased(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1)
lbdown = false;
if(wasdragged)
wasdragged = false;
if(Math.abs(momentum) <= 4.0) // then assume that the mouse wasn't moving (much) when the button was released
return;
// otherwise 'throw' the scroll
int max = vbar.getMaximum();
int count;
double brakingforce = 1.04; // set an initial braking force
for(count = 1000; count > 0; count--){ // don't allow it to accidentally go on forever
momentum = momentum / brakingforce; // apply the brake
if(Math.abs(momentum) < 1.5)
brakingforce = 1.02; // ease off the brake towards the end (gives a slight overrun ala iOS)
if(Math.abs(momentum) < 1.0) // bring it to a halt
break;
int val = vbar.getValue();
if(val < 0 || val > max) // prevent overrun
break;
vbar.setValue((int) momentum + val); // increment the scroll bar
try {
Thread.sleep(10); // slow the loop down so the user can see the scroll
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
public void mouseMoved(MouseEvent e) {}
public void mouseClicked(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
}
Here is a 'bare bones' example of how you might apply it:
panel = new JPanel();
jlist = new JList(list);
scroller = new TouchScroll(jlist);
panel.add(scroller);
Any components that respond to a mouse release may need to do something like this:
public void mouseReleased(MouseEvent e) {
if(scroller.wasDragged())
return;
actionENTER();
}
Lastly, as mentioned earlier, components that consume mouse events will need
to inform the scroller like this:
JCheckBox checkbox = new JCheckBox(text);
checkbox.addMouseMotionListener(scroller);
checkbox.addMouseListener(scroller);
You can use JavaFX's touch event framework.
Here is some sample code from the linked tutorial on handling scroll events:
rect.setOnScroll(new EventHandler<ScrollEvent>() {
#Override public void handle(ScrollEvent event) {
if (!event.isInertia()) {
rect.setTranslateX(rect.getTranslateX() + event.getDeltaX());
rect.setTranslateY(rect.getTranslateY() + event.getDeltaY());
}
log("Rectangle: Scroll event" +
", inertia: " + event.isInertia() +
", direct: " + event.isDirect());
event.consume();
}
});
rect.setOnScrollStarted(new EventHandler<ScrollEvent>() {
#Override public void handle(ScrollEvent event) {
inc(rect);
log("Rectangle: Scroll started event");
event.consume();
}
});
rect.setOnScrollFinished(new EventHandler<ScrollEvent>() {
#Override public void handle(ScrollEvent event) {
dec(rect);
log("Rectangle: Scroll finished event");
event.consume();
}
});
I have a JTable and I need to be able to reorder the columns. However I want the first column to not be able to be re-ordered. I used the following to enable reordering:
table.getTableHeader().setReorderingAllowed(true);
The columns can now be reordered including the first column which I don't want. Is there any way to lock the first column?
I have seen some solutions that use two tables with the first column being in a separate table, but maybe there's a better/simpler way.
This is the solution that I used to prevent the 1st column from being re-ordered
private int columnValue = -1;
private int columnNewValue = -1;
tblResults.getColumnModel().addColumnModelListener(new TableColumnModelListener()
{
public void columnAdded(TableColumnModelEvent e) {}
public void columnMarginChanged(ChangeEvent e) {}
public void columnMoved(TableColumnModelEvent e)
{
if (columnValue == -1)
columnValue = e.getFromIndex();
columnNewValue = e.getToIndex();
}
public void columnRemoved(TableColumnModelEvent e) {}
public void columnSelectionChanged(ListSelectionEvent e) {}
});
tblResults.getTableHeader().addMouseListener(new MouseAdapter()
{
#Override
public void mouseReleased(MouseEvent e)
{
if (columnValue != -1 && (columnValue == 0 || columnNewValue == 0))
tblResults.moveColumn(columnNewValue, columnValue);
columnValue = -1;
columnNewValue = -1;
}
});
Cheers,
Nearly 4 years later, there's still no optimal solution in sight anywhere.
Yet another suboptimal approach to prevent dragging of the first column (and other columns over the first) is to intercept the mouseEvents before the mouseInputListener installed by the uidelegate can handle them (similar to a recent QA).
The collaborators
a custom MouseMotionListener which delegates all events to the originally installed, except the dragged if it would lead to another column above the first
replace the original with the custom
update the replacement whenever the LAF is changed (because the original is controlled by the ui). This requires subclassing of JTableHeader and do the wiring in updateUI
The custom MouseInputListener:
/**
* A delegating MouseInputListener to be installed instead of
* the one registered by the ui-delegate.
*
* It's implemented to prevent dragging the first column or any other
* column over the first.
*/
public static class DragHook implements MouseInputListener {
private JTableHeader header;
private MouseListener mouseDelegate;
private MouseMotionListener mouseMotionDelegate;
private int maxX;
public DragHook(JTableHeader header) {
this.header = header;
installHook();
}
/**
* Implemented to do some tweaks/bookkeeping before/after
* passing the event to the original
*
* - temporarily disallow reordering if hit on first column
* - calculate the max mouseX that's allowable in dragging to the left
*
*/
#Override
public void mousePressed(MouseEvent e) {
int index = header.columnAtPoint(e.getPoint());
boolean reorderingAllowed = header.getReorderingAllowed();
if (index == 0) {
// temporarily disable re-ordering
header.setReorderingAllowed(false);
}
mouseDelegate.mousePressed(e);
header.setReorderingAllowed(reorderingAllowed);
if (header.getDraggedColumn() != null) {
Rectangle r = header.getHeaderRect(index);
maxX = header.getColumnModel().getColumn(0).getWidth()
+ e.getX() - r.x -1;
}
}
/**
* Implemented to pass the event to the original only if the
* mouseX doesn't lead to dragging the column over the first.
*/
#Override
public void mouseDragged(MouseEvent e) {
TableColumn dragged = header.getDraggedColumn();
int index = getViewIndexForColumn(header.getColumnModel(), dragged);
// dragged column is at second position, allow only drags to the right
if (index == 1) {
if (e.getX() < maxX) return;
}
mouseMotionDelegate.mouseDragged(e);
}
//-------- delegating-only methods
#Override
public void mouseReleased(MouseEvent e) {
mouseDelegate.mouseReleased(e);
}
#Override
public void mouseClicked(MouseEvent e) {
mouseDelegate.mouseClicked(e);
}
#Override
public void mouseEntered(MouseEvent e) {
mouseDelegate.mouseEntered(e);
}
#Override
public void mouseExited(MouseEvent e) {
mouseDelegate.mouseExited(e);
}
#Override
public void mouseMoved(MouseEvent e) {
mouseMotionDelegate.mouseMoved(e);
}
//------------ un-/install listeners
protected void installHook() {
installMouseHook();
installMouseMotionHook();
}
protected void installMouseMotionHook() {
MouseMotionListener[] listeners = header.getMouseMotionListeners();
for (int i = 0; i < listeners.length; i++) {
MouseMotionListener l = listeners[i];
if (l.getClass().getName().contains("TableHeaderUI")) {
this.mouseMotionDelegate = l;
listeners[i] = this;
}
header.removeMouseMotionListener(l);
}
for (MouseMotionListener l : listeners) {
header.addMouseMotionListener(l);
}
}
protected void installMouseHook() {
MouseListener[] listeners = header.getMouseListeners();
for (int i = 0; i < listeners.length; i++) {
MouseListener l = listeners[i];
if (l.getClass().getName().contains("TableHeaderUI")) {
this.mouseDelegate = l;
listeners[i] = this;
}
header.removeMouseListener(l);
}
for (MouseListener l : listeners) {
header.addMouseListener(l);
}
}
public void uninstallHook() {
uninstallMouseHook();
uninstallMouseMotionHook();
}
protected void uninstallMouseMotionHook() {
MouseMotionListener[] listeners = header.getMouseMotionListeners();
for (int i = 0; i < listeners.length; i++) {
MouseMotionListener l = listeners[i];
if (l == this) {
listeners[i] = mouseMotionDelegate;
}
header.removeMouseMotionListener(l);
}
for (MouseMotionListener l : listeners) {
header.addMouseMotionListener(l);
}
}
protected void uninstallMouseHook() {
MouseListener[] listeners = header.getMouseListeners();
for (int i = 0; i < listeners.length; i++) {
MouseListener l = listeners[i];
if (l == this) {
listeners[i] = mouseDelegate;
}
header.removeMouseListener(l);
}
for (MouseListener l : listeners) {
header.addMouseListener(l);
}
}
}
Usage which survives switching of LAF, f.i.:
JTable table = new JTable(new AncientSwingTeam()) {
#Override
protected JTableHeader createDefaultTableHeader() {
JTableHeader header = new JTableHeader(getColumnModel()) {
DragHook hook;
#Override
public void updateUI() {
if (hook != null) {
hook.uninstallHook();
hook = null;
}
super.updateUI();
hook = new DragHook(this);
}
};
return header;
}
};
I think that you need to override the columnMoved() method in TableColumnModelListener. the TableColumnModelEvent class has a getFromIndex() method that you should be able to look at to determine if it's your fixed column, and then you should be able to cancel the event.
Hope that helps. A
First you need to define a better and simpler way. What don't you like about the 2 table approach?
You can't use a TableColumnModelListener, because the event is fired "after" the column has already been moved.
The code for dragging the column is found in the BasicTableHeaderUI. So you could try overriding the code there, but then you would need to do it for all LAFs.
The above code invokes JTableHeader.getReorderingAllowed() on a mousePressed event to determine if column reordering is allowed. I guess you could override that method in the JTableHeader and perhaps use the MouseInfo class to get the current mouse location to determine if it was over the first column and then return false. But then now you would also need to create a custom JTable that uses the custom table header.
Of course with either of the above suggestions you might be able to prevent the first column from being moved. But don't forget you also need to prevent the 2nd column from being inserted before the first column. I don't believe there is a short simple answer to the question.
Fixed Column Table is my version of how this would be imlemented with two tables. Is it better? I don't know, but it is simple since its only a single line of code to use it.
I had the same issue, and I was searching about it. So far I found two ways of doing that.
The "if I was rewriting it myself" method : Modifying the base classes from Java.
TableColumn would need a new property, like the "resizingAllowed", it would need the "reorderingAllowed".
From this, the modifications take place in BasicTableHeaderUI :
There is already :
private static boolean canResize(TableColumn column,
JTableHeader header) {
return (column != null) && header.getResizingAllowed()
&& column.getResizable();
}
It would need too :
private static boolean canMove(TableColumn column,
JTableHeader header) {
return (column != null) && header.getReorderingAllowed()
&& column.getReorderable();
}
(Note that if you don't want the first column only to not move, you can do without changing the TableColumns :
private static boolean canMove(TableColumn column,
JTableHeader header) {
return (column != null) && header.getReorderingAllowed()
&& header.getColumnModel().getColumnIndex(column.getIdentifier()) != 0;
}
)
After, two places to modify in the MouseInputListener :
in the mousePressed, calling the canMove() instead of the header.getReorderingAllowed(). This ensures that a column which shouldn't be moved, won't be.
But this is not enough, we need to prevent the immobile columns from being moved during dragging another one. You need to change the mouseDragged, too, when it is getting the "newColumnIndex" :
if (0 < newColumnIndex && newColumnIndex < cm.getColumnCount())
You need to add the condition if this new index can be moved, for example using the "canMove()" method. This way, when you will drag a column to this immobile one, you will still drag it, but it won't swap them.
Note that this method would require you to explicitly set the UI for the JTableHeader used for your JTable, which is not really ideal. But this is the most adapted though, as it deals with the problem on the place it is supposed to.
The "Let's try to block the normal behavior with what we actually have" method : Not modifying the UI, this method focus on the JTableHeader to block the commands made by the UI.
First, to block dragging the first column, we need a subclass from JTableHeader, with this overridden method :
#Override
public void setDraggedColumn(TableColumn pAColumn)
{
int lIndex = -1;
if (pAColumn != null)
lIndex = getColumnModel().getColumnIndex(pAColumn.getIdentifier());
if (lIndex != 0)
super.setDraggedColumn(pAColumn);
}
This will prevent a user from dragging the first column. But like described earlier, this is only one part of the problem, we need to prevent another dragged column from swapping with this first one.
So far, I don't have a correct method for this. I tried by subclassing the TableColumnModel, and overriding the moveColumn() method :
#Override
public void moveColumn(int pColumnIndex, int pNewIndex)
{
//Move only if the first column is not concerned
if (pColumnIndex =! 0 && pNewIndex != 0)
super.moveColumn(pColumnIndex, pNewIndex);
}
But this won't work, as the UI will update anyway the mouse position in the mouseDragged method, you will have a jump from your dragged column to another place.
So I'm still searching, and wonder if someone has propositions concerning this part.
At first, I used the very last Gnoupi's suggestion consisting in subclassing the TableColumnModel and overriding moveColumn but there were still some annoying jumps.
This is "my" fully working and tested solution with no nasty jump, it mainly relies on StanislavKo and kleopatra's suggestions. I added a more complicated mechanism to revert the unwanted move when releasing the mouse button :
table.getTableHeader().setUI(new WindowsTableHeaderUI() {
#Override
protected MouseInputListener createMouseInputListener() {
return new BasicTableHeaderUI.MouseInputHandler() {
#Override
public void mouseDragged(MouseEvent e) {
if (header.isEnabled() && header.getReorderingAllowed() && header.getDraggedColumn() != null && header.getDraggedColumn().getModelIndex() == frozenColumnModelIndex) {
header.setDraggedDistance(0);
header.setDraggedColumn(null);
return;
}
super.mouseDragged(e);
}
#Override
public void mouseReleased(MouseEvent e) {
if (header.isEnabled() && header.getReorderingAllowed() && header.getDraggedColumn() != null &&
0 <= illegalTableColumnMoveFromIndex && illegalTableColumnMoveFromIndex < header.getTable().getColumnModel().getColumnCount()) {
header.setDraggedDistance(0);
header.setDraggedColumn(null);
header.getTable().getColumnModel().moveColumn(illegalTableColumnMoveToIndex, illegalTableColumnMoveFromIndex);
illegalTableColumnMoveFromIndex = -1;
illegalTableColumnMoveToIndex = -1;
return;
}
super.mouseReleased(e);
}
};
}
});
table.getColumnModel().addColumnModelListener(new TableColumnModelListener() {
#Override
public void columnAdded(TableColumnModelEvent e) {
}
#Override
public void columnRemoved(TableColumnModelEvent e) {
}
#Override
public void columnMoved(TableColumnModelEvent e) {
if (e.getFromIndex() != e.getToIndex() && table.getColumnModel().getColumn(e.getFromIndex()).getModelIndex() == frozenColumnModelIndex) {
illegalTableColumnMoveFromIndex = e.getFromIndex();
illegalTableColumnMoveToIndex = e.getToIndex();
} else {
illegalTableColumnMoveFromIndex = -1;
illegalTableColumnMoveToIndex = -1;
}
}
#Override
public void columnMarginChanged(ChangeEvent e) {
}
#Override
public void columnSelectionChanged(ListSelectionEvent e) {
}
});
Note that the latest valid move is accepted instead of completely reverting the column drag.
frozenColumnModelIndex is the index of the "frozen" column in the table model.
illegalTableColumnMoveFromIndex is the index of the column from where it was moved when the latest illegal move was detected.
illegalTableColumnMoveToIndex is the index of the column to where it was moved when the latest illegal move was detected.
The code inside mouseDragged is enough to prevent the frozen column from being dragged, the rest allows to prevent another column from being dragged to the frozen column.
It works as is under Microsoft Windows as I extend WindowsTableHeaderUI but rather use the reflection API to set the mouse input listener of the table header UI, call uninstallerListeners() and finally call header.addMouseListener(mouseInputListener) and header.addMouseMotionListener(mouseInputListener) in order to drive my solution cross-platform without making any assumption on the name of the class for each table header UI.
I admit it might be a bit less robust than kleopatra's solution. I thank you all for your help, I'm really grateful and I'm really happy to see that collaborative work just works :)
I have used the "The 'Let's try to block the normal behavior with what we actually have' method" approach. Gnoupi said that he did not solve the second part of the problem. Here is the solution for just Windows XP L&F:
copy XPStyle class to yourself.
extend WindowsTableHeaderUI. Take a look at the source code.
use it: getTableHeader().setUI(new TreeTableWindowsTableHeaderUI());
Thanks to Gnoupi for the efforts.
I will just put the column back after the move is complete. So something like.
#Override
public void moveColumn(int from, int to) {
super.moveColumn(from, to);
if (from == 0 || to == 0) {
super.moveColumn(to, from);
}
}