Extending Swing's ToolTipManager to change behaviour on hover? - java

I'd like to implement a ToolTip in Swing that has customised behaviour: the longer the user hovers over the component, the more detail should be shown in the tooltip (i.e., a few new lines are added after a few seconds of the user hovering over the component). I just need to check whether this is really doable with Swing without things getting too messy. My idea at the moment would probably be:
Extend ToolTipManager
Override mouseEntered to start a timer (maybe use javax.swing.Timer?). Call setToolTipText and createToolTip to refresh the tooltip and add new information at regular intervals
Override mouseExited to reset the timer
Probably use setDismissDelay to set the dismiss delay to something a lot longer (or Integer.MAX_VALUE)
Is such a thing feasible or is this not a good way to work with Swing (sorry, I'm pretty new to it)? Is there a better way of doing this?
[edit] Hmm, just remembered that ToolTipManager is a singleton with a constructor that only has package visibility, so it can't be extended.
[edit 2] I'm trying out a few solutions at the moment. One thing that I forgot to add is that I do need to know which component is being hovered over - which I guess means I'll need to be working with some sort of listener with a mouseEntered() method (or be able to access this information). And no other interactivity with the popup/tooltip is needed - it just needs to display information.

(This may seem a bit confusing so let me know if you need me to clarify let me know and I'll try to show you how I picture the code) I think your idea might work like if you extend it, and also make a private class that extends Threadand then in the run() method you do something like
while(true)
{
sleep(1);
timeElapsed++;
}
And in your class that extends ToolTipManager, create a field for that class that extends Thread and in the mouseEntered(MouseEvent e) instantiate the thing like:
extendsThreadClass = new ExtendsThreadClass();
extendsThreadClass.start();
and then in the mouseExited(MouseEvent e) method do
extendsThreadClass = null;
Then in that mouseEntered(MouseEvent e) method after starting the Thread then you can do what you want to do after the time thing like
if(timeElapsed > 3000)
{
//what you want to do here
}
Sorry it may be confusing, let me know if i can clear it up for you

I thought I should update this with the approach I took before I saw l1zZY's answer (which I think is the better way of doing things - I still had bugs in my code before I moved onto something else, but this might still be helpful to someone). This is what I did:
Extend JToolTip
Use a Swing Timer for timing
Add a MouseMotion listener to the JTree (in my case I wanted the popup to show when a node was hovered over)
Somewhat inelegantly, detect when the mouse moved over a tree node like this:
public void mouseMoved(MouseEvent e) {
int x = (int) e.getX();
int y = (int) e.getY();
TreePath path = getPathForLocation(x, y);
if (path == null) {
tooltip.hide();
} else {
TreeNode node = (TreeNode) path.getLastPathComponent();
tooltip.setHoveredNode(node);
if (!tooltip.isVisible) {
int absX = e.getXOnScreen();
int absY = e.getYOnScreen();
final Popup tooltipContainer = PopupFactory.getSharedInstance().getPopup(PDTreeView.this,
tooltip, absX, absY);
tooltip.setToolTipContainer(tooltipContainer);
tooltip.show();
}
}
}
tooltip.show() refers to how the tooltip was contained in a Popup
in order to show or hide it programmatically. show() shows the
Popup (and therefore tooltip) and also starts the Swing timer.
Timer has a method called actionPerformed() which is called at whatever interval you set. I just had that method call the code that adds new information to the tooltip. in hide(), I reset the tooltip and the timer.
I had issues with the popup or tooltip not resizing to fit the content, but otherwise this seemed ok.

Related

MouseListener across several panels?

My user story is the following:
In order to upgrade a tower, the player has to drag a gun from the shop and drop it on the tower.
In practice, the GameView contains both the BattlefieldCanvas and the ShopView. Besides, the ShopView contains a GunSelector for each buyable gun. All those guys are sub-classes of JPanel.
I'm currently using a MousListener to handle several actions performed on the BattlefieldCanvas; I thought I could use the same stuff to handle a mouse trip from one of the GunSelector to the BattlefieldCanvas ( = across several panels ), so I tried to add the same MousListener to the gun selectors and the battlefield.
Problem: doesn't work. The getSource() method of the event object returns a reference to the gun selector while the mouse is actually released on the battlefield.
PS: Unlike gun selectors, towers are not swing components but images drawn by the paintComponent method.
1) To answer to your original question as to why source is still the component mouse was clicked on. You just need to read the JavaDoc:
public Object getSource()
The object on which the Event initially occurred.
Returns:
The object on which the Event initially occurred.
2) Now, how can we get the actual component that mouse is released on. You can try this approach:
#Override
public void mouseReleased(MouseEvent e) {
Component theCOmponentMouseIsReleasedOn = frame.findComponentAt( e.getLocationOnScreen() );
}
You don't need to always call findComponentAt on frame, you can call on the container that contains your BattlefieldCanvas.

How can I fix the actionPerformed method freezing when the mouse is moved within the pane?

In a Swing application when the mouse is moved within the frame, the actionPerformed method stops cycling. How can I fix this?
Here's the basic layout of my program:
ActionListener taskPerformer = new ActionListener() {
public void actionPerformed(ActionEvent evt) {
// main game loop
}
}
public void paintComponent(Graphics g) {
// render loop
}
I found a similar question here. The user found that by lowering the polling rate of the mouse they fixed the problem; however I cannot change the polling rate on my apple trackpad, and no other solutions were offered. Also it is an inelegant solution that would require the user to change settings, and honestly there has to be a better way to fix the problem.
Basically the question boils down to this:
Is there a way for me to change the polling rate from within my program? I did some research and couldn't find a solution.
How can I disable mouse movement events, so as to not slow down my game loop? (Also perhaps move it to a separate process, and use the mouses x and y position provided by that process for logic in the game loop.)
What alternate solution can I implement to fix this problem?
I think you need to implement the "ActionListener" where you can take it, because when you are moving will work the ActionListener, when you will click, it will be already ActionEvent.
Also you can get more from:
https://docs.oracle.com/javase/tutorial/uiswing/events/mouselistener.html
and
How can I get the location of the mouse pointer in a JPanel (Without any operation of the Mouse)?

how to create a JDialog which can only floating within its parent frame

I want to create a JDialog which can only float within its parent Frame. That is it cannot be dragged out from its parent frame. Any idea? mouse-motion listener?
thanks,
EDIT:
My applicaiton is based on frame not internal frame, so I cannot use JInternalFrame
I need a non-modal dialog so, I cannot use JOptionPane with internal feature.
Use JInternalFrame instead. See this page for an example.
Ok, you should have specified the "hidden" problem in the initial question.
I've have found a very tricky solution, I don't suggest to use it except from developing a better one, perhaps starting from this.
Given an JInternalFrame, provide it with a componentMove listener to inhibit moving it in hidden positions.
As far as I've tested it, it has refresh problems (maybe they can be solved) and the stability in extreme case to assess too.
Provided "as is" for further improvement, not as a nice piece of software :-)
public void componentMoved(ComponentEvent e) {
Rectangle r = new Rectangle();
MyInternalFrame mif = MyInternalFrame.this;
JDesktopPane dp = mif.getDesktopPane();
if (mif.getX() + mif.getWidth()> dp.getWidth()) {
mif.setLocation(mif.getDesktopPane().getWidth()-mif.getWidth(),mif.getY());
}
if (mif.getY() + mif.getHeight()> dp.getHeight()) {
mif.setLocation(mif.getX(), mif.getDesktopPane().getHeight()-mif.getHeight());
}
if (mif.getX()<0) {
mif.setLocation(0, mif.getY());
}
if (mif.getY()<0) {
mif.setLocation(mif.getX(), 0);
}
}
});

Java Swing: Do something when a component has *finished* resizing

Apologies for the somewhat unclear question - couldn't think of a better way of putting it.
I use a JXTaskPane (from the Swing labs extension API) to display some information.
The user can "click" the title to expand the panel. The JXTaskPane is in a container JPanel, which is then added to a JFrame, my main application window.
I want my application window to resize to the size of the expanded task pane. To achieve this, I added a component listener to my container JPanel which would set size to the now expanded panel.
panel.addComponentListener(new ComponentListener()
{
public void componentResized(ComponentEvent e)
{
Dimension newSize = ((JXTaskPane)e.getSource()).getSize();
reSizeFrame(newSize);
}
}
private void reSizeFrame(Dimension newSize)
{
if ((newSize.height < maxSize.height) && (newSize.width < maxSize.width))
{
containerPanel.setSize(newSize);
appFrame.setSize(containerPanel.getSize());
appFrame.pack();
}
}
The problem is that the componentResized method is called as the task pane expands, as a result the resizeFrame method is called lots of times, and looks really awful on the screen.
How can I detect when the JXTaskpane has finished resizing? I thought of two approaches:
Put the resizeFrame() method in a SwingUtilities.invokeLate(..) call.
Put in a timer resizeFrame call, so any subsequent calls do not do anything until the timer fires. This should give enough time for the panel to resize.
What is the best way forward?
Also - This is my first serious Java GUI app after years of server side program. StackOverflow has been very helpful. So thanks!
I know you've already selected an answer, but overriding the paint method is definitely not correct, and while you may be able to hack something in place, it won't be ideal.
Looking at the source for JXTaskPane and specifically looking in setExpanded() (line 387), you can see it calls JXCollapsiblePane.setCollapsed(...) and then fires a property change event for expanded. A listener on that property won't be correct, because it'll fire before the animation is complete. So, if you go into JXCollapsiblePane and look at setCollapsed(...) (line 470) you'll see that if it's animated, it sets the paramaters and starts a timer. We want to know when the animation ends, so in that file, look at the animator (line 620, and specifically 652-667), which shows that when the animation ends, it fires a property change for ANIMATION_STATE_KEY with a value of "collapsed" or "expanded". This is the event you actually want. However, you don't have access to JXCollapsiblePane, so go back to JXTaskPane and search for ANIMATION_STATE_KEY, and you find line 208, which shows that JXTaskPane creates a listener on JXCollapsiblePane.ANIMATION_STATE_KEY and refires it as it's own event.
Since you do have access to JXTaskPane, you can listen for that event, so doing ...
taskPane.addPropertyChangeListener(JXCollapsiblePane.ANIMATION_STATE_KEY, new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent e) {
if(e.getNewValue().equals("expanded") {
...
}
else if(e.getNewValue().equals("collapsed") {
...
}
}
}
should get your event exactly when you want it.
The correct way to listen for events in Swing is through property listeners. Unfortunately, the only way to find out what the correct properties and values are is by digging through source code.
As a suggestion, have you tried overriding the paint method, first calling super and then putting your resize code at the end of that if (and only if) the size has changed significantly.
I'm not familiar with JXTaskPane, but my first reaction is that maybe you're handling the wrong event. You want the frame to resize when the user clicks on the header - so why not handle that event (perhaps using EventQueue.invokeLater() to resize the frame after the task pane has been resized)?
But if that doesn't work and you need to use the approach you've outlined above, using a javax.swing.Timer is probably best. Set it for 200 milliseconds or so and just restart() it every time componentResized() fires.

When is a Swing component 'displayable'?

Is there a way (e.g., via an event?) to determine when a Swing component becomes 'displayable' -- as per the Javadocs for Component.getGraphics?
The reason I'm trying to do this is so that I can then call getGraphics(), and pass that to my 'rendering strategy' for the component.
I've tried adding a ComponentListener, but componentShown doesn't seem to get called. Is there anything else I can try?
Thanks.
And additionally, is it OK to keep hold of the Graphics object I receive? Or is there potential for a new one to be created later in the lifetime of the Component? (e.g., after it is resized/hidden?)
Add a HierarchyListener
public class MyShowingListener {
private JComponent component;
public MyShowingListener(JComponent jc) { component=jc; }
public void hierarchyChanged(HierarchyEvent e) {
if((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED)>0 && component.isShowing()) {
System.out.println("Showing");
}
}
}
JTable t = new JTable(...);
t.addHierarchyListener(new MyShowingListener(t));
You can listen for a resize event. When a component is first displayed, it is resized from 0,0 to whatever the layout manager determines (if it has one).
You need to check up the component hierarchy. Check after AncestorListener.ancestorAdded is called.
I've always used Coomponent.addNotify to know when the component is ready to be rendered.Not sure if is the the best way, but it works for me. Of course you must subclass the component.
Component.isDisplayable should be the right answer but I know it didn't worked for me as I thought it will(I don't remember why, but there was something and I switched to addNotify).
Looking in the SUN's source code, I can see addNotify fires a HierarchyEvent.SHOWING_CHANGED so this is the best way to be notified.

Categories