JAL-2621 Added custom drag cursor to overview panel
[jalview.git] / src / jalview / gui / OverviewPanel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.bin.Cache;
24 import jalview.renderer.OverviewRenderer;
25 import jalview.util.MessageManager;
26 import jalview.util.Platform;
27 import jalview.viewmodel.OverviewDimensions;
28 import jalview.viewmodel.OverviewDimensionsHideHidden;
29 import jalview.viewmodel.OverviewDimensionsShowHidden;
30 import jalview.viewmodel.ViewportListenerI;
31
32 import java.awt.BorderLayout;
33 import java.awt.Cursor;
34 import java.awt.Dimension;
35 import java.awt.Image;
36 import java.awt.Point;
37 import java.awt.event.ActionEvent;
38 import java.awt.event.ActionListener;
39 import java.awt.event.ComponentAdapter;
40 import java.awt.event.ComponentEvent;
41 import java.awt.event.MouseAdapter;
42 import java.awt.event.MouseEvent;
43 import java.awt.event.MouseMotionAdapter;
44 import java.beans.PropertyChangeEvent;
45 import java.beans.PropertyVetoException;
46
47 import javax.swing.ImageIcon;
48 import javax.swing.JCheckBoxMenuItem;
49 import javax.swing.JInternalFrame;
50 import javax.swing.JPanel;
51 import javax.swing.JPopupMenu;
52 import javax.swing.SwingUtilities;
53
54 /**
55  * Panel displaying an overview of the full alignment, with an interactive box
56  * representing the viewport onto the alignment.
57  * 
58  * @author $author$
59  * @version $Revision$
60  */
61 public class OverviewPanel extends JPanel
62         implements Runnable, ViewportListenerI
63 {
64   protected Image dragImage = new ImageIcon(
65           getClass().getResource("/images/dragcursor.png"))
66                   .getImage();
67
68   private OverviewDimensions od;
69
70   private OverviewCanvas oviewCanvas;
71
72   private AlignViewport av;
73
74   private AlignmentPanel ap;
75
76   private JCheckBoxMenuItem displayToggle;
77
78   private boolean showHidden = true;
79
80   private boolean draggingBox = false;
81
82   private ProgressPanel progressPanel;
83
84   /**
85    * Creates a new OverviewPanel object.
86    * 
87    * @param alPanel
88    *          The alignment panel which is shown in the overview panel
89    */
90   public OverviewPanel(AlignmentPanel alPanel)
91   {
92     this.av = alPanel.av;
93     this.ap = alPanel;
94
95     showHidden = Cache.getDefault(Preferences.SHOW_OV_HIDDEN_AT_START,
96             true);
97     if (showHidden)
98     {
99       od = new OverviewDimensionsShowHidden(av.getRanges(),
100             (av.isShowAnnotation()
101                     && av.getAlignmentConservationAnnotation() != null));
102     }
103     else
104     {
105       od = new OverviewDimensionsHideHidden(av.getRanges(),
106               (av.isShowAnnotation()
107                       && av.getAlignmentConservationAnnotation() != null));
108     }
109
110     setLayout(new BorderLayout());
111     progressPanel = new ProgressPanel(OverviewRenderer.UPDATE,
112             MessageManager.getString("label.oview_calc"), getWidth());
113     this.add(progressPanel, BorderLayout.SOUTH);
114     oviewCanvas = new OverviewCanvas(od, av, progressPanel);
115
116     add(oviewCanvas, BorderLayout.CENTER);
117
118     av.getRanges().addPropertyChangeListener(this);
119
120     // without this the overview window does not size to fit the overview canvas
121     setPreferredSize(new Dimension(od.getWidth(), od.getHeight()));
122
123     // set up custom cursor for use on Mac...
124     Point hotSpot = new Point(16, 16);
125     String cursorName = "Custom drag cursor";
126     
127     Cursor tempCursor;
128     if (Platform.isAMac())
129     {
130       tempCursor = getToolkit().createCustomCursor(dragImage, hotSpot,
131               cursorName);
132     }
133     else
134     {
135       tempCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
136     }
137     final Cursor dragCursor = tempCursor;
138     
139     addComponentListener(new ComponentAdapter()
140     {
141       @Override
142       public void componentResized(ComponentEvent evt)
143       {
144         // Resize is called on the initial display of the overview.
145         // This code adjusts sizes to account for the progress bar if it has not
146         // already been accounted for, which triggers another resize call for
147         // the correct sizing, at which point the overview image is updated.
148         // (This avoids a double recalculation of the image.)
149         if (getWidth() == od.getWidth() && getHeight() == od.getHeight()
150                 + progressPanel.getHeight())
151         {
152           updateOverviewImage();
153         }
154         else
155         {
156           if ((getWidth() > 0) && (getHeight() > 0))
157           {
158             od.setWidth(getWidth());
159             od.setHeight(getHeight() - progressPanel.getHeight());
160           }
161
162           setPreferredSize(new Dimension(od.getWidth(),
163                   od.getHeight() + progressPanel.getHeight()));
164         }
165       }
166
167     });
168
169     addMouseMotionListener(new MouseMotionAdapter()
170     {
171       @Override
172       public void mouseDragged(MouseEvent evt)
173       {
174         if (!SwingUtilities.isRightMouseButton(evt))
175         {
176           if (draggingBox)
177           {
178             // set the mouse position as a fixed point in the box
179             // and drag relative to that position
180             od.adjustViewportFromMouse(evt.getX(), evt.getY(),
181                     av.getAlignment().getHiddenSequences(),
182                     av.getAlignment().getHiddenColumns());
183           }
184           else
185           {
186             od.updateViewportFromMouse(evt.getX(), evt.getY(),
187                     av.getAlignment().getHiddenSequences(),
188                     av.getAlignment().getHiddenColumns());
189           }
190         }
191       }
192
193       @Override
194       public void mouseMoved(MouseEvent evt)
195       {
196         if (od.isPositionInBox(evt.getX(), evt.getY()))
197         {
198           // display drag cursor at mouse position
199           getParent().setCursor(dragCursor);
200         }
201         else
202         {
203           // reset cursor
204           getParent().setCursor(
205                   Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
206         }
207       }
208
209     });
210
211     addMouseListener(new MouseAdapter()
212     {
213       @Override
214       public void mousePressed(MouseEvent evt)
215       {
216         if (SwingUtilities.isRightMouseButton(evt))
217         {
218           if (!Platform.isAMac())
219           {
220             showPopupMenu(evt);
221           }
222         }
223         else
224         {
225           // don't do anything if the mouse press is in the overview's box
226           // (wait to see if it's a drag instead)
227           // otherwise update the viewport
228           if (!od.isPositionInBox(evt.getX(), evt.getY()))
229           {
230             draggingBox = false;
231             od.updateViewportFromMouse(evt.getX(), evt.getY(),
232                     av.getAlignment().getHiddenSequences(),
233                     av.getAlignment().getHiddenColumns());
234           }
235           else
236           {
237             draggingBox = true;
238             od.setDragPoint(evt.getX(), evt.getY(),
239                     av.getAlignment().getHiddenSequences(),
240                     av.getAlignment().getHiddenColumns());
241           }
242         }
243       }
244
245       @Override
246       public void mouseClicked(MouseEvent evt)
247       {
248         if (SwingUtilities.isRightMouseButton(evt))
249         {
250           showPopupMenu(evt);
251         }
252         // click in box should also reset the cursor
253         else if (od.isPositionInBox(evt.getX(), evt.getY()))
254         {
255           // display drag cursor at mouse position
256           getParent().setCursor(dragCursor);
257         }
258         else
259         {
260           // reset cursor
261           getParent().setCursor(
262                   Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
263         }
264       }
265     });
266   }
267
268   /*
269    * Displays the popup menu and acts on user input
270    */
271   private void showPopupMenu(MouseEvent e)
272   {
273     JPopupMenu popup = new JPopupMenu();
274     ActionListener menuListener = new ActionListener()
275     {
276       @Override
277       public void actionPerformed(ActionEvent event)
278       {
279         // switch on/off the hidden columns view
280         toggleHiddenColumns();
281         displayToggle.setSelected(showHidden);
282       }
283     };
284     displayToggle = new JCheckBoxMenuItem(
285             MessageManager.getString("label.togglehidden"));
286     displayToggle.setEnabled(true);
287     displayToggle.setSelected(showHidden);
288     popup.add(displayToggle);
289     displayToggle.addActionListener(menuListener);
290     popup.show(this, e.getX(), e.getY());
291   }
292
293   /*
294    * Toggle overview display between showing hidden columns and hiding hidden columns
295    */
296   private void toggleHiddenColumns()
297   {
298     if (showHidden)
299     {
300       showHidden = false;
301       od = new OverviewDimensionsHideHidden(av.getRanges(),
302               (av.isShowAnnotation()
303                       && av.getAlignmentConservationAnnotation() != null));
304     }
305     else
306     {
307       showHidden = true;
308       od = new OverviewDimensionsShowHidden(av.getRanges(),
309               (av.isShowAnnotation()
310                       && av.getAlignmentConservationAnnotation() != null));
311     }
312     oviewCanvas.resetOviewDims(od);
313     updateOverviewImage();
314     setBoxPosition();
315   }
316
317   /**
318    * Updates the overview image when the related alignment panel is updated
319    */
320   public void updateOverviewImage()
321   {
322     if (oviewCanvas == null)
323     {
324       /*
325        * panel has been disposed
326        */
327       return;
328     }
329
330     if ((getWidth() > 0) && (getHeight() > 0))
331     {
332       od.setWidth(getWidth());
333       od.setHeight(getHeight() - progressPanel.getHeight());
334     }
335     
336     setPreferredSize(new Dimension(od.getWidth(),
337             od.getHeight() + progressPanel.getHeight()));
338
339     if (oviewCanvas.restartDraw())
340     {
341       return;
342     }
343
344     Thread thread = new Thread(this);
345     thread.start();
346     repaint();
347
348     
349   }
350
351   @Override
352   public void run()
353   {
354     if (oviewCanvas != null)
355     {
356       oviewCanvas.draw(av.isShowSequenceFeatures(),
357               (av.isShowAnnotation()
358                       && av.getAlignmentConservationAnnotation() != null),
359               ap.getSeqPanel().seqCanvas.getFeatureRenderer());
360       setBoxPosition();
361     }
362   }
363
364   /**
365    * Update the overview panel box when the associated alignment panel is
366    * changed
367    * 
368    */
369   private void setBoxPositionOnly()
370   {
371     if (od != null)
372     {
373       int oldX = od.getBoxX();
374       int oldY = od.getBoxY();
375       int oldWidth = od.getBoxWidth();
376       int oldHeight = od.getBoxHeight();
377       od.setBoxPosition(av.getAlignment().getHiddenSequences(),
378               av.getAlignment().getHiddenColumns());
379       repaint(oldX - 1, oldY - 1, oldWidth + 2, oldHeight + 2);
380       repaint(od.getBoxX(), od.getBoxY(), od.getBoxWidth(),
381               od.getBoxHeight());
382     }
383   }
384
385   private void setBoxPosition()
386   {
387     if (od != null)
388     {
389       od.setBoxPosition(av.getAlignment().getHiddenSequences(),
390               av.getAlignment().getHiddenColumns());
391       repaint();
392     }
393   }
394
395   @Override
396   public void propertyChange(PropertyChangeEvent evt)
397   {
398     setBoxPositionOnly();
399   }
400
401   /**
402    * Removes this object as a property change listener, and nulls references
403    */
404   protected void dispose()
405   {
406     try
407     {
408       if (av != null)
409       {
410         av.getRanges().removePropertyChangeListener(this);
411       }
412
413       oviewCanvas.dispose();
414
415       /*
416        * close the parent frame (which also removes it from the
417        * Desktop Windows menu)
418        */
419       ((JInternalFrame) SwingUtilities.getAncestorOfClass(
420               JInternalFrame.class, (this))).setClosed(true);
421     } catch (PropertyVetoException e)
422     {
423       // ignore
424     } finally
425     {
426       progressPanel = null;
427       av = null;
428       oviewCanvas = null;
429       ap = null;
430       od = null;
431     }
432   }
433 }