JTextPane prevents scrolling in the parent JScrollPane - java

I have the following "tree" of objects:
JPanel
JScrollPane
JPanel
JPanel
JScrollPane
JTextPane
When using the mouse wheel to scroll over the outer JScrollPane I encounter one annoying problem. As soon as the mouse cursor touches the inner JScrollPane, it seems that the scrolling events get passed into that JScrollPane and are not processed anymore by the first one. That means that scrolling the "parent" JScrollPane stops.
Is it possible to disable only the mouse wheel on the inner JScrollPane? Or even better, disable scrolling if there is nothing to scroll (most of the time the textpane only contains 1-3 lines of text), but enable it if there is more content?

I have run into this annoying problem also, and Sbodd's solution was not acceptable for me because I needed to be able to scroll inside tables and JTextAreas. I wanted the behavior to be the same as a browser, where the mouse over a scrollable control will scroll that control until the control bottoms out, then continue to scroll the parent scrollpane, usually the scrollpane for the whole page.
This class will do just that. Just use it in place of a regular JScrollPane. I hope it helps you.
/**
* A JScrollPane that will bubble a mouse wheel scroll event to the parent
* JScrollPane if one exists when this scrollpane either tops out or bottoms out.
*/
public class PDControlScrollPane extends JScrollPane {
public PDControlScrollPane() {
super();
addMouseWheelListener(new PDMouseWheelListener());
}
class PDMouseWheelListener implements MouseWheelListener {
private JScrollBar bar;
private int previousValue = 0;
private JScrollPane parentScrollPane;
private JScrollPane getParentScrollPane() {
if (parentScrollPane == null) {
Component parent = getParent();
while (!(parent instanceof JScrollPane) && parent != null) {
parent = parent.getParent();
}
parentScrollPane = (JScrollPane)parent;
}
return parentScrollPane;
}
public PDMouseWheelListener() {
bar = PDControlScrollPane.this.getVerticalScrollBar();
}
public void mouseWheelMoved(MouseWheelEvent e) {
JScrollPane parent = getParentScrollPane();
if (parent != null) {
/*
* Only dispatch if we have reached top/bottom on previous scroll
*/
if (e.getWheelRotation() < 0) {
if (bar.getValue() == 0 && previousValue == 0) {
parent.dispatchEvent(cloneEvent(e));
}
} else {
if (bar.getValue() == getMax() && previousValue == getMax()) {
parent.dispatchEvent(cloneEvent(e));
}
}
previousValue = bar.getValue();
}
/*
* If parent scrollpane doesn't exist, remove this as a listener.
* We have to defer this till now (vs doing it in constructor)
* because in the constructor this item has no parent yet.
*/
else {
PDControlScrollPane.this.removeMouseWheelListener(this);
}
}
private int getMax() {
return bar.getMaximum() - bar.getVisibleAmount();
}
private MouseWheelEvent cloneEvent(MouseWheelEvent e) {
return new MouseWheelEvent(getParentScrollPane(), e.getID(), e
.getWhen(), e.getModifiers(), 1, 1, e
.getClickCount(), false, e.getScrollType(), e
.getScrollAmount(), e.getWheelRotation());
}
}
}

Inspired by the existing answers, I
took the code from Nemi's answer
combined it with kleopatra's answer to a similar question to avoid constructing the MouseWheelEvent verbosely
extracted the listener into its own top-level class so that it can be used in contexts where the JScrollPane class cannot be extended
inlined the code as far as possible.
The result is this piece of code:
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
/**
* Passes mouse wheel events to the parent component if this component
* cannot scroll further in the given direction.
* <p>
* This behavior is a little better than Swing's default behavior but
* still worse than the behavior of Google Chrome, which remembers the
* currently scrolling component and sticks to it until a timeout happens.
*
* #see Stack Overflow
*/
public final class MouseWheelScrollListener implements MouseWheelListener {
private final JScrollPane pane;
private int previousValue;
public MouseWheelScrollListener(JScrollPane pane) {
this.pane = pane;
previousValue = pane.getVerticalScrollBar().getValue();
}
public void mouseWheelMoved(MouseWheelEvent e) {
Component parent = pane.getParent();
while (!(parent instanceof JScrollPane)) {
if (parent == null) {
return;
}
parent = parent.getParent();
}
JScrollBar bar = pane.getVerticalScrollBar();
int limit = e.getWheelRotation() < 0 ? 0 : bar.getMaximum() - bar.getVisibleAmount();
if (previousValue == limit && bar.getValue() == limit) {
parent.dispatchEvent(SwingUtilities.convertMouseEvent(pane, e, parent));
}
previousValue = bar.getValue();
}
}
It is used like this:
JScrollPane pane = new JScrollPane();
pane.addMouseWheelListener(new MouseWheelScrollListener(pane));
Once an instance of this class is created and bound to a scroll pane, it cannot be reused for another component since it remembers the previous position of the vertical scroll bar.

Sadly, the obvious solution (JScrollPane.setWheelScrollingEnabled(false)) doesn't actually deregister for MouseWheelEvents, so it doesn't achieve the effect you want.
Here's a crude-hackery way of disabling scrolling altogether that will let the MouseWheelEvents reach the outer JScrollPane:
for (MouseWheelListener mwl : scrollPane.getMouseWheelListeners()) {
scrollPane.removeMouseWheelListener(mwl);
}
If you do this to your inner JScrollPane, it'll never respond to scroll wheel events; the outer JScrollPane will get all of them.
If you want to do it "cleanly", you'd need to implement your own ScrollPaneUI, and set that as the JScrollPane's UI with setUI(). Unfortunately, you can't just extend BasicScrollPaneUI and disable its mouse wheel listener, because the relevant member variables are private and there aren't any flags or guards on the ScrollPaneUI's installation of its MouseWheelListener.
For your "even better" solution, you'd have to dig deeper than I have time to into the ScrollPaneUI, find the hooks where the scrollbars get made visible / invisible, and add/remove your MouseWheelListener at those points.
Hope that helps!

#Nemi has a good solution already.
I boiled it down a bit further, putting the follwing method in my library:
static public void passMouseWheelEventsToParent(final Component pComponent, final Component pParent) {
pComponent.addMouseWheelListener((final MouseWheelEvent pE) -> {
pParent.dispatchEvent(new MouseWheelEvent(pParent, pE.getID(), pE.getWhen(), pE.getModifiers(), 1, 1, pE.getClickCount(), false, pE.getScrollType(), pE.getScrollAmount(), pE.getWheelRotation()));
});
}

Related

How to create interactive panels with changing textures and colors in java?

I'm new here but I did some research before posting. My goal is to create a simple tower defense game using a couple of interesting ideas and moreover to train my development skills using javax.swing and java.awt. As far as I know, developers are mostly lazy guys and they do everything to make their life more simple.
There is map with a grid and for map loading my game uses a boolean matrix and a loading method to locate terrain on panels. I thought it will be quite simple solution. Because the matrix is 12 x 12, I would like to create it with some other application rather than entering a line of 144 numbers.
Here comes an idea to first create a map editor application and later do maps for levels in it. When I have such a tool I could make that map visually and then save its boolean matrix to a file, which later can be read by loading method and recreated in game. Next step is to make graphics and also panels that would react properly on user's actions. On the left there is a panel with buttons - after user clicks one of them, the field currentColor changes.
This field is used by method that implements actionListener and makes color change of the panel that is declared in its constructor. I wanted to change color of certain panel when its clicked. I use colors because its easier for now to make it working, later I want to replace color with a texture - obviously, I know I have to use a paintComponent method, but I assume that will work for it too, right? Also would be nice if the panel border changes color when I move my cursor over it and changes it back to normal when mouse is somewhere else.
The point here is that I'm having some trouble to make panels interactive. First problem is that panels are created in for loop and that makes it difficult to refer to a certain panel while mouse is over it. Another one comes with that I would like to change appearance of that panel after I click on it.
As far as I know, MouseListeners should do the work, but how to actually write it to have an effect on screen? I found some post about that, but for me it doesn't work. Here's the link: highlighting panels in java
My code:
import javax.swing.*;
import javax.swing.border.LineBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class Editor extends JFrame
{
private JButton towers = new JButton(new ImageIcon("pu.gif"));
private JButton road = new JButton(new ImageIcon("pu.gif"));
private JButton start = new JButton(new ImageIcon("pu.gif"));
private JButton finish = new JButton(new ImageIcon("pu.gif"));
private String mapTitle = "testmap";
private Color currentColor;
private int width = Toolkit.getDefaultToolkit().getScreenSize().width;
private int height = Toolkit.getDefaultToolkit().getScreenSize().height;
private String currentMapType = "Standard";
private static final int currentHeight = 12;
private static final int currentWidth = 12;
private JPanel[][] currentMapPanel;
private int[][] currentMapField;
//Toolbar - a panel with buttons
private JPanel panel = new JPanel(new GridLayout(10,3));
//Container for map - a panel with map
private Dimension containerSize = new Dimension(height, height);
static JPanel container = new JPanel(new GridLayout(currentHeight, currentWidth), true);
//Separator
private JSplitPane separator = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, panel, container);
public Editor()
{
initComponents();
}
public void initComponents()
{
this.setTitle(mapTitle + ".map" + " - " + "Game Map Editor");
this.setSize(800, 600);
int frameWidth = this.getSize().width;
int frameHeight = this.getSize().height;
this.setLocation((width - frameWidth) / 2, (height - frameHeight) / 2);
this.setIconImage(Toolkit.getDefaultToolkit().getImage("pu.gif"));
towers.addActionListener(e -> {
currentColor = Color.CYAN;
System.out.println(currentColor);
});
road.addActionListener(e -> {
currentColor = Color.GRAY;
System.out.println(currentColor);
});
start.addActionListener(e -> {
currentColor = Color.LIGHT_GRAY;
System.out.println(currentColor);
});
finish.addActionListener(e -> {
currentColor = Color.BLACK;
System.out.println(currentColor);
});
new Map(currentMapType, currentWidth, currentHeight, false);
panel.add(towers);
panel.add(road);
panel.add(start);
panel.add(finish);
this.getContentPane().add(separator);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
}
/**
* Class that allows to load the graphic map and to view it in JFrame
*/
public class Map
{
public Map(String mapType, int rows, int columns, boolean load)
{
if (!load)
{
currentMapPanel = mapPanel(rows, columns);
currentMapField = new MapGenerator().mapFieldEmpty(rows, columns);
mapLoader(currentMapField, currentMapPanel);
}
else
{
currentMapPanel = mapPanel(rows, columns);
currentMapField = new MapGenerator().mapFieldGenerator(rows, columns);
mapLoader(currentMapField, currentMapPanel);
}
}
private JPanel[][] mapPanel(int rows, int columns)
{
JPanel[][] mapPanel = new JPanel[rows][columns];
for (int i = 0; i < rows - 1; i++)
{
for (int j = 0; j < columns - 1; j++)
{
mapPanel[i][j] = new JPanel(true);
mapPanel[i][j].setPreferredSize(new Dimension(height/12, height/12));
mapPanel[i][j].setBorder(new LineBorder(Color.BLACK));
mapPanel[i][j].addMouseListener(new MouseAdapter() {
#Override
public void mouseEntered(MouseEvent e) {
super.mouseEntered(e);
JPanel parent = (JPanel) e.getSource();
new colorListener(parent, Color.LIGHT_GRAY);
parent.revalidate();
}
#Override
public void mouseExited(MouseEvent e)
{
super.mouseExited(e);
JPanel parent = (JPanel) e.getSource();
new colorListener(parent, Color.GREEN);
parent.revalidate();
}
#Override
public void mouseClicked(MouseEvent e)
{
super.mouseClicked(e);
JPanel parent = (JPanel) e.getSource();
new colorListener(parent, currentColor);
parent.revalidate();
}
});
}
}
return mapPanel;
}
private void mapLoader(int[][] mapField, JPanel[][] mapPanel)
{
for (int i = 0; i < mapField.length - 1; i++)
{
for (int j = 0; j < mapField.length - 1; j++)
{
if (mapField[i][j] == 0)
{
mapPanel[i][j].setBackground(Color.GREEN);
container.add(mapPanel[i][j]);
}
else if (mapField[i][j] == 1)
{
mapPanel[i][j].setBackground(Color.GRAY);
container.add(mapPanel[i][j]);
}
else if (mapField[i][j] == 2)
{
mapPanel[i][j].setBackground(Color.LIGHT_GRAY);
container.add(mapPanel[i][j]);
}
else if (mapField[i][j] == 3)
{
mapPanel[i][j].setBackground(Color.BLACK);
container.add(mapPanel[i][j]);
}
else
{
System.out.println("An error occurred...");
}
}
}
}
private JPanel mapContainer(int rows, int columns)
{
container = new JPanel();
container.setLayout(createLayout(rows, columns));
container.setPreferredSize(containerSize);
container.setBounds(height/4, height/4, containerSize.width, containerSize.height);
return container;
}
private GridLayout createLayout(int rows, int columns){
GridLayout layout = new GridLayout(rows, columns);
return layout;
}
}
private class colorListener implements ActionListener
{
public colorListener(JPanel p, Color c)
{
this.panel = p;
this.color = c;
}
#Override
public void actionPerformed(ActionEvent e) {
panel.setBackground(color);
}
JPanel panel;
Color color;
}
public static void main(String[] args) {
new Editor().setVisible(true);
}
}
The question is broad and the answer complicated.
Essentially, you want to do some research into concepts such as "separation of responsibilities" and "decoupling code".
The idea is that you break down you functionality requirements so that your objects are doing a single, specialised job. You also "decouple" the code so that changing the implementation of one part won't adversely affect other parts of the program. This is commonly achieved through the use of interfaces.
You will also want to investigate the concept of "model-view-controller", where by the "data" or "state" is modelled in one or more classes, but is wholly independent of the UI. The UI is then free to "render" the model in what ever way it feels is appropriate.
In this way, the "view" (interacting with the controller) can change the state (or react to the change in state) of the model, making it easier to mange (no seriously, it does)
Code Review ...
This...
static JPanel container = new JPanel(new GridLayout(currentHeight, currentWidth), true);
is dangerous and a bad idea. It voids the concept of encapsulation and allows any one to create new instance of container at any time, without notification, which will disconnect it from what the program was previously using. In fact, you actually do this.
static is not your friend. Used correctly, it's useful, but used in this way, it's just a bad idea and should be avoid.
You should instead favour "dependency injection", where the "elements" that any one object relies on are passed to it.
I would avoid things like...
this.setSize(800, 600);
int frameWidth = this.getSize().width;
int frameHeight = this.getSize().height;
this.setLocation((width - frameWidth) / 2, (height - frameHeight) / 2);
Windows are complicated components, which also contain window decorations which wrap about the content. This means that the available space to the content is window size - window decorations. Instead. You should rely on the layout manager API to provide appropriate sizing hints and pack the frame.
On most modern OSs you have "other" system elements, which, again, reduces the amount of available space on the screen (docks, task bars, other funky stuff). Instead, you can use setLocationRelativeTo(null) to centre the window more reliably on the screen.
Instead of setIconImage, you should be using Window#setIconImages(List), which allows you to pass a number of images which can be used by the API to represent the application in different places that require different resolution images.
Not sure what ...
new Map(currentMapType, currentWidth, currentHeight, false);
but it's not really helping.
If you find yourself just creating an instance of class without actually maintaining a reference to it, then it's probably a good sign of a bad design.
Your Map class raises a bunch of questions which aren't easily answered. It kind of makes me worried that the Map class is modifying the state of the parent class and screams "dependency injection" instead.
This...
mapPanel[i][j].setPreferredSize(new Dimension(height / 12, height / 12));
is best avoided. You should prefer overriding getPreferredSize and it should simply return a "desired" size, which could then be used by things like GridLayout to layout the component more effectively.
This then leads into the "separation of responsibility". This section suggestions you should have a "tile" class, which would be self managed and responsible for a single element from the model.
There are a number of things wrong with your mouse event handling...
#Override
public void mouseEntered(MouseEvent e) {
super.mouseEntered(e);
JPanel parent = (JPanel) e.getSource();
new colorListener(parent, Color.LIGHT_GRAY);
parent.revalidate();
}
You shouldn't be calling super.mouseXxx(e) on of the jobs of those methods is to call the delegate MouseListeners, so, mess right there.
You can more easily use e.getComponent() to get a reference to the component which generated the event, but if panel was a self contained unit of work (ie Tile) and the MouseListener an anonymous or inner class, you'd be able to forego the cast altogether.
new colorListener(parent, Color.LIGHT_GRAY); scares me as it's setting up a bunch of strongly references objects which can't be easily dereferenced, nor am I clear on there intent.
parent.revalidate(); isn't doing what you seem to think it's doing.
revalidate generates a new layout pass, what you seem to want is repaint.
These...
container.setPreferredSize(containerSize);
container.setBounds(height / 4, height / 4, containerSize.width, containerSize.height);
are just bad ideas. Let the content of the container, along with the layout manager deal with.
So, the short answer is, you have a lot of research left to do, things like:
OO design patterns
OO good practices, including "separation of responsibilities", "code decoupling" and in a more general sense, "dependency injection"
Model-View-Controller, coding to interface instead of implementation
just to name a few

JTable with autoresize, horizontal scrolling and shrinkable first column

I am having trouble creating a JTable with scrollbars.
I want a JTable with 2 columns and no visible scrollbars.
If I enlarge one of the columns the scrollbars should become visible and the columns resize.
I followed this answer How to make JTable both AutoResize and horizontall scrollable? and works fine which basically comes down to:
JTable table = new JTable() {
#Override
public boolean getScrollableTracksViewportWidth() {
return getPreferredSize().width < getParent().getWidth();
}
};
table.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
However, with this solution I cannot shrink the first column. Only if I enlarge the 2nd column and the scrollbars become visible I can shrink the first one.
The required behavior is that the 2 columns are automatically resizable. Meaning that the 1 column can shrink and afterwards extend without the scrollbars popping up. Only when extending one of the columns, so that the view should extend, the scrollbars should pop up.
A scenario:
Shrink the 1st column -> 2nd one enlarges, no scrollbars
Enlarge the 1st column -> 2nd one shrinks, still no scrollbars
Enlarge the 2nd column -> 1 column stays the same, 2nd one enlarges and scrollbars appear
Any ideas on fixing this?
An SSCCE:
import javax.swing.JDialog;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.WindowConstants;
import javax.swing.table.AbstractTableModel;
import java.awt.BorderLayout;
import java.awt.Container;
public class TableTest {
public TableTest() {
JDialog mainDialog = new JDialog();
mainDialog.setResizable( true );
mainDialog.setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE );
Container contentPane = mainDialog.getContentPane();
JTable myTable = new JTable() {
#Override
public boolean getScrollableTracksViewportWidth() {
return getPreferredSize().width < getParent().getWidth();
}
};
myTable.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
myTable.setModel( new MyTableModel() );
JScrollPane scrollPane = new JScrollPane( myTable );
contentPane.add( scrollPane, BorderLayout.CENTER );
mainDialog.pack();
mainDialog.setVisible( true );
}
public static void main( String[] args ) {
new TableTest();
}
private class MyTableModel extends AbstractTableModel {
#Override public int getRowCount() {
return 1;
}
#Override public int getColumnCount() {
return 2;
}
#Override public Object getValueAt( int rowIndex, int columnIndex ) {
return "ARandomValue";
}
}
}
It's not quite enough to override the getTracks method, you have to fool super's layout into doing the right-thingy if tracking:
JTable myTable = new JTable(10, 4) {
private boolean inLayout;
#Override
public boolean getScrollableTracksViewportWidth() {
return hasExcessWidth();
}
#Override
public void doLayout() {
if (hasExcessWidth()) {
// fool super
autoResizeMode = AUTO_RESIZE_SUBSEQUENT_COLUMNS;
}
inLayout = true;
super.doLayout();
inLayout = false;
autoResizeMode = AUTO_RESIZE_OFF;
}
protected boolean hasExcessWidth() {
return getPreferredSize().width < getParent().getWidth();
}
#Override
public void columnMarginChanged(ChangeEvent e) {
if (isEditing()) {
// JW: darn - cleanup to terminate editing ...
removeEditor();
}
TableColumn resizingColumn = getTableHeader().getResizingColumn();
// Need to do this here, before the parent's
// layout manager calls getPreferredSize().
if (resizingColumn != null && autoResizeMode == AUTO_RESIZE_OFF
&& !inLayout) {
resizingColumn.setPreferredWidth(resizingColumn.getWidth());
}
resizeAndRepaint();
}
};
Might not be entirely complete (probably still isn't, even after the edit to take care of columnMarginChanged, copied from JXTable (of the SwingX project) which support that behaviour by an additional layout property
xTable.setHorizontalScrollEnabled(true);
With the implementation of #kleopatra, I noticed that you get a scrollbar, when you reduce the size of a column and then increase it again just slightly (which happens quite often by accident). So I've slightly changed the code slightly:
protected boolean hasExcessWidth() {
return getPreferredSize().width - getParent().getWidth() < 50;
}
This allows to slowly increase the size of a column without loosing the auto resize.
Not really sure yet if the magic "50" is a good measurement, but works quite well in initial tests

Scrolling Through a JPanel When Mouse is Hovering Over JTable

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);
}
}
}

Detecting mouse enter/exit events anywhere on JPanel

Basically there is a JPanel on which I want to know when the mouse enters the area of the JPanel and exits the area of the JPanel. So I added a mouse listener, but if there are components on the JPanel and the mouse goes over one of them it is detected as an exit on the JPanel, even though the component is on the JPanel. I was wondering whether anyone knows any way to solve this problem without doing something like adding listeners onto all components on the JPanel?
There is a very easy solution for this problem that can work :
public class MyJPanel implements MouseListener {
public void mouseExited(MouseEvent e) {
java.awt.Point p = new java.awt.Point(e.getLocationOnScreen());
SwingUtilities.convertPointFromScreen(p, e.getComponent());
if(e.getComponent().contains(p)) {return;}
...//the rest of your code
}
...
}
This way you just ignore the mouseExited event when it occurs on a child element.
Here is one way to do it for a component that may contain other components:
Add a global AWT event listener to get all mouse events. For example:
Toolkit.getDefaultToolkit().addAWTEventListener(
new TargetedMouseHandler( panel ), AWTEvent.MOUSE_EVENT_MASK );
Implement the TargetedMouseHandler to ignore events that aren't sourced by the panel or by one of the panel's children (you can use SwingUtilities.isDescendingFrom to test for this).
Keep track of whether or not the mouse is already within the bounds of your panel. When you get a MouseEvent.MOUSE_ENTERED event in your panel or one of its children, set a flag to true.
When you get a MouseEvent.MOUSE_EXITED event, only reset the flag if the point in the MouseEvent is outside the bounds of your target panel. SwingUtilities.convertPoint and Component.getBounds().contains() will come in handy here.
This is sample code implementing Ash's solution. For me, the JFrame did not detect all exit events properly, but an inner JPanel did, so I passed in two components - one for testing descendants and one for testing the boundary.
Toolkit.getDefaultToolkit().addAWTEventListener(
new TargetedMouseHandler(this, this.jPanel),
AWTEvent.MOUSE_EVENT_MASK);
}
public class TargetedMouseHandler implements AWTEventListener
{
private Component parent;
private Component innerBound;
private boolean hasExited = true;
public TargetedMouseHandler(Component p, Component p2)
{
parent = p;
innerBound = p2;
}
#Override
public void eventDispatched(AWTEvent e)
{
if (e instanceof MouseEvent)
{
if (SwingUtilities.isDescendingFrom(
(Component) e.getSource(), parent))
{
MouseEvent m = (MouseEvent) e;
if (m.getID() == MouseEvent.MOUSE_ENTERED)
{
if (hasExited)
{
System.out.println("Entered");
hasExited = false;
}
} else if (m.getID() == MouseEvent.MOUSE_EXITED)
{
Point p = SwingUtilities.convertPoint(
(Component) e.getSource(),
m.getPoint(),
innerBound);
if (!innerBound.getBounds().contains(p))
{
System.out.println("Exited");
hasExited = true;
}
}
}
}
}
}
A simpeler solution with java 1.8+
public class MyJPanel implements MouseListener {
public void mouseExited(MouseEvent e) {
if(!this.contains(e.getPoint())) {
... //the rest of your code
}
}
...
}
If you want to get all events sent to a top-level window you can add a listener to the glass pane of the JFrame. See getGlassPane.

Remove Arrows from Swing Scrollbar in JScrollPane

I would like to remove the scrollbar arrow buttons from a scrollbar in a JScrollPane. How would I do this?
class NoArrowScrollBarUI extends BasicScrollBarUI {
protected JButton createZeroButton() {
JButton button = new JButton("zero button");
Dimension zeroDim = new Dimension(0,0);
button.setPreferredSize(zeroDim);
button.setMinimumSize(zeroDim);
button.setMaximumSize(zeroDim);
return button;
}
#Override
protected JButton createDecreaseButton(int orientation) {
return createZeroButton();
}
#Override
protected JButton createIncreaseButton(int orientation) {
return createZeroButton();
}
#Override
protected void paintTrack(Graphics g, JComponent c, Rectangle trackBounds) {
//own painting if needed
}
#Override
protected void paintThumb(Graphics g, JComponent c, Rectangle thumbBounds) {
//own painting if needed
}
}
Removing buttons let space for then. I found make buttons zero as the simplest way.
If you are using the basic version of JScrollBar, then it is probably rendering using the BasicScrollBarUI. I would suggest that you extend BasicScrollBarUI to create a custom UI class (like MyBasicScrollBarUI) . The buttons are protected variables in the superclass. So you need to override the installComponents() methods in the subclass and make sure that you do not add the buttons. See the below code snippet and hide the lines as suggested there.
protected void installComponents(){
switch (scrollbar.getOrientation()) {
case JScrollBar.VERTICAL:
incrButton = createIncreaseButton(SOUTH);
decrButton = createDecreaseButton(NORTH);
break;
case JScrollBar.HORIZONTAL:
if (scrollbar.getComponentOrientation().isLeftToRight()) {
incrButton = createIncreaseButton(EAST);
decrButton = createDecreaseButton(WEST);
} else {
incrButton = createIncreaseButton(WEST);
decrButton = createDecreaseButton(EAST);
}
break;
}
scrollbar.add(incrButton); // Comment out this line to hide arrow
scrollbar.add(decrButton); // Comment out this line to hide arrow
// Force the children's enabled state to be updated.
scrollbar.setEnabled(scrollbar.isEnabled());
}
Then, in your code after you initialize a JScrollBar, you can call setUI() and pass in an instance of MyBasicScrollBarUI class.
Note: I havent tried this myself, but from the code it looks like it could work.
It is not the most elegant way... but works for me
JScrollBar jsb = getHorizontalScrollBar();
for(Component c : jsb.getComponents()) {
jsb.remove(c);
}
This is the way i went.
Set the scrollbar policy of the scrollbar you want to hide as never
Mimic this behavior with a MouseWheelListener
This method:
Is fast to implement with very few lines of code.
Retains the benefits of the L&F.
Will remove both the buttons and the bar.
Below is a sample code for removing the verticall scroll bar.
JScrollPane myScrollPane = new JScrollPane();
//remove the scroll bar you don't want
myScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
JTextPane myJTextArea = new JTextPane();
//myScrollPane.setViewportView(myJTextArea);
myScrollPane.addMouseWheelListener(new MouseWheelListener() {
//this will mimick the behavior of scrolling
public void mouseWheelMoved(MouseWheelEvent e) {
JScrollBar scrollBar = myScrollPane.getVerticalScrollBar();
//capturing previous value
int previousValue = scrollBar.getValue();
int addAmount;
//decide where the wheel scrolled
//depending on how fast you want to scroll
//you can chane the addAmount to something greater or lesser
if(e.getWheelRotation()>0) {
addAmount = 2;
}else {
addAmount = -2;
}
//set the new value
scrollBar.setValue(previousValue + addAmount);
}
});

Categories

Resources