JAL-2840 fix wrapped scrolling when using cursor at a hidden region
[jalview.git] / src / jalview / viewmodel / ViewportRanges.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.viewmodel;
22
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.HiddenColumns;
25
26 /**
27  * Slightly less embryonic class which: Supplies and updates viewport properties
28  * relating to position such as: start and end residues and sequences; ideally
29  * will serve hidden columns/rows too. Intention also to support calculations
30  * for positioning, scrolling etc. such as finding the middle of the viewport,
31  * checking for scrolls off screen
32  */
33 public class ViewportRanges extends ViewportProperties
34 {
35   public static final String STARTRES = "startres";
36
37   public static final String ENDRES = "endres";
38
39   public static final String STARTSEQ = "startseq";
40
41   public static final String ENDSEQ = "endseq";
42
43   private boolean wrappedMode = false;
44
45   // start residue of viewport
46   private int startRes;
47
48   // end residue of viewport
49   private int endRes;
50
51   // start sequence of viewport
52   private int startSeq;
53
54   // end sequence of viewport
55   private int endSeq;
56
57   // alignment
58   private AlignmentI al;
59
60   /**
61    * Constructor
62    * 
63    * @param alignment
64    *          the viewport's alignment
65    */
66   public ViewportRanges(AlignmentI alignment)
67   {
68     // initial values of viewport settings
69     this.startRes = 0;
70     this.endRes = alignment.getWidth() - 1;
71     this.startSeq = 0;
72     this.endSeq = alignment.getHeight() - 1;
73     this.al = alignment;
74   }
75
76   /**
77    * Get alignment width in cols, including hidden cols
78    */
79   public int getAbsoluteAlignmentWidth()
80   {
81     return al.getWidth();
82   }
83
84   /**
85    * Get alignment height in rows, including hidden rows
86    */
87   public int getAbsoluteAlignmentHeight()
88   {
89     return al.getHeight() + al.getHiddenSequences().getSize();
90   }
91
92   /**
93    * Get alignment width in cols, excluding hidden cols
94    */
95   public int getVisibleAlignmentWidth()
96   {
97     return al.getWidth() - al.getHiddenColumns().getSize();
98   }
99
100   /**
101    * Get alignment height in rows, excluding hidden rows
102    */
103   public int getVisibleAlignmentHeight()
104   {
105     return al.getHeight();
106   }
107
108   /**
109    * Set first residue visible in the viewport, and retain the current width.
110    * Fires a property change event.
111    * 
112    * @param res
113    *          residue position
114    */
115   public void setStartRes(int res)
116   {
117     int width = getViewportWidth();
118     setStartEndRes(res, res + width - 1);
119   }
120
121   /**
122    * Set start and end residues at the same time. This method only fires one
123    * event for the two changes, and should be used in preference to separate
124    * calls to setStartRes and setEndRes.
125    * 
126    * @param start
127    *          the start residue
128    * @param end
129    *          the end residue
130    */
131   public void setStartEndRes(int start, int end)
132   {
133     int oldstartres = this.startRes;
134
135     /*
136      * if not wrapped, don't leave white space at the right margin
137      */
138     int lastColumn = getVisibleAlignmentWidth() - 1;
139     if (!wrappedMode && (start > lastColumn))
140     {
141       startRes = Math.max(lastColumn, 0);
142     }
143     else if (start < 0)
144     {
145       startRes = 0;
146     }
147     else
148     {
149       startRes = start;
150     }
151
152     int oldendres = this.endRes;
153     if (end < 0)
154     {
155       endRes = 0;
156     }
157     else if (!wrappedMode && (end > lastColumn))
158     {
159       endRes = Math.max(lastColumn, 0);
160     }
161     else
162     {
163       endRes = end;
164     }
165
166     changeSupport.firePropertyChange(STARTRES, oldstartres, startRes);
167     if (oldstartres == startRes)
168     {
169       // event won't be fired if start positions are same
170       // fire an event for the end positions in case they changed
171       changeSupport.firePropertyChange(ENDRES, oldendres, endRes);
172     }
173   }
174
175   /**
176    * Set the first sequence visible in the viewport, maintaining the height. If
177    * the viewport would extend past the last sequence, sets the viewport so it
178    * sits at the bottom of the alignment. Fires a property change event.
179    * 
180    * @param seq
181    *          sequence position
182    */
183   public void setStartSeq(int seq)
184   {
185     int startseq = seq;
186     int height = getViewportHeight();
187     if (startseq + height - 1 > getVisibleAlignmentHeight() - 1)
188     {
189       startseq = getVisibleAlignmentHeight() - height;
190     }
191     setStartEndSeq(startseq, startseq + height - 1);
192   }
193
194   /**
195    * Set start and end sequences at the same time. The viewport height may
196    * change. This method only fires one event for the two changes, and should be
197    * used in preference to separate calls to setStartSeq and setEndSeq.
198    * 
199    * @param start
200    *          the start sequence
201    * @param end
202    *          the end sequence
203    */
204   public void setStartEndSeq(int start, int end)
205   {
206     int oldstartseq = this.startSeq;
207     int visibleHeight = getVisibleAlignmentHeight();
208     if (start > visibleHeight - 1)
209     {
210       startSeq = Math.max(visibleHeight - 1, 0);
211     }
212     else if (start < 0)
213     {
214       startSeq = 0;
215     }
216     else
217     {
218       startSeq = start;
219     }
220
221     int oldendseq = this.endSeq;
222     if (end >= visibleHeight)
223     {
224       endSeq = Math.max(visibleHeight - 1, 0);
225     }
226     else if (end < 0)
227     {
228       endSeq = 0;
229     }
230     else
231     {
232       endSeq = end;
233     }
234
235     changeSupport.firePropertyChange(STARTSEQ, oldstartseq, startSeq);
236     if (oldstartseq == startSeq)
237     {
238       // event won't be fired if start positions are the same
239       // fire in case the end positions changed
240       changeSupport.firePropertyChange(ENDSEQ, oldendseq, endSeq);
241     }
242   }
243
244   /**
245    * Set the last sequence visible in the viewport. Fires a property change
246    * event.
247    * 
248    * @param seq
249    *          sequence position
250    */
251   public void setEndSeq(int seq)
252   {
253     int height = getViewportHeight();
254     setStartEndSeq(seq - height + 1, seq);
255   }
256
257   /**
258    * Get start residue of viewport
259    */
260   public int getStartRes()
261   {
262     return startRes;
263   }
264
265   /**
266    * Get end residue of viewport
267    */
268   public int getEndRes()
269   {
270     return endRes;
271   }
272
273   /**
274    * Get start sequence of viewport
275    */
276   public int getStartSeq()
277   {
278     return startSeq;
279   }
280
281   /**
282    * Get end sequence of viewport
283    */
284   public int getEndSeq()
285   {
286     return endSeq;
287   }
288
289   /**
290    * Set viewport width in residues, without changing startRes. Use in
291    * preference to calculating endRes from the width, to avoid out by one
292    * errors! Fires a property change event.
293    * 
294    * @param w
295    *          width in residues
296    */
297   public void setViewportWidth(int w)
298   {
299     setStartEndRes(startRes, startRes + w - 1);
300   }
301
302   /**
303    * Set viewport height in residues, without changing startSeq. Use in
304    * preference to calculating endSeq from the height, to avoid out by one
305    * errors! Fires a property change event.
306    * 
307    * @param h
308    *          height in sequences
309    */
310   public void setViewportHeight(int h)
311   {
312     setStartEndSeq(startSeq, startSeq + h - 1);
313   }
314
315   /**
316    * Set viewport horizontal start position and width. Use in preference to
317    * calculating endRes from the width, to avoid out by one errors! Fires a
318    * property change event.
319    * 
320    * @param start
321    *          start residue
322    * @param w
323    *          width in residues
324    */
325   public void setViewportStartAndWidth(int start, int w)
326   {
327     int vpstart = start;
328     if (vpstart < 0)
329     {
330       vpstart = 0;
331     }
332
333     /*
334      * if not wrapped, don't leave white space at the right margin
335      */
336     if (!wrappedMode)
337     {
338       if ((w <= getVisibleAlignmentWidth())
339               && (vpstart + w - 1 > getVisibleAlignmentWidth() - 1))
340       {
341         vpstart = getVisibleAlignmentWidth() - w;
342       }
343
344     }
345     setStartEndRes(vpstart, vpstart + w - 1);
346   }
347
348   /**
349    * Set viewport vertical start position and height. Use in preference to
350    * calculating endSeq from the height, to avoid out by one errors! Fires a
351    * property change event.
352    * 
353    * @param start
354    *          start sequence
355    * @param h
356    *          height in sequences
357    */
358   public void setViewportStartAndHeight(int start, int h)
359   {
360     int vpstart = start;
361     if (vpstart < 0)
362     {
363       vpstart = 0;
364     }
365     else if ((h <= getVisibleAlignmentHeight())
366             && (vpstart + h - 1 > getVisibleAlignmentHeight() - 1))
367     // viewport height is less than the full alignment and we are running off
368     // the bottom
369     {
370       vpstart = getVisibleAlignmentHeight() - h;
371     }
372     setStartEndSeq(vpstart, vpstart + h - 1);
373   }
374
375   /**
376    * Get width of viewport in residues
377    * 
378    * @return width of viewport
379    */
380   public int getViewportWidth()
381   {
382     return (endRes - startRes + 1);
383   }
384
385   /**
386    * Get height of viewport in residues
387    * 
388    * @return height of viewport
389    */
390   public int getViewportHeight()
391   {
392     return (endSeq - startSeq + 1);
393   }
394
395   /**
396    * Scroll the viewport range vertically. Fires a property change event.
397    * 
398    * @param up
399    *          true if scrolling up, false if down
400    * 
401    * @return true if the scroll is valid
402    */
403   public boolean scrollUp(boolean up)
404   {
405     /*
406      * if in unwrapped mode, scroll up or down one sequence row;
407      * if in wrapped mode, scroll by one visible width of columns
408      */
409     if (up)
410     {
411       if (wrappedMode)
412       {
413         pageUp();
414       }
415       else
416       {
417         if (startSeq < 1)
418         {
419           return false;
420         }
421         setStartSeq(startSeq - 1);
422       }
423     }
424     else
425     {
426       if (wrappedMode)
427       {
428         pageDown();
429       }
430       else
431       {
432         if (endSeq >= getVisibleAlignmentHeight() - 1)
433         {
434           return false;
435         }
436         setStartSeq(startSeq + 1);
437       }
438     }
439     return true;
440   }
441
442   /**
443    * Scroll the viewport range horizontally. Fires a property change event.
444    * 
445    * @param right
446    *          true if scrolling right, false if left
447    * 
448    * @return true if the scroll is valid
449    */
450   public boolean scrollRight(boolean right)
451   {
452     if (!right)
453     {
454       if (startRes < 1)
455       {
456         return false;
457       }
458
459       setStartRes(startRes - 1);
460     }
461     else
462     {
463       if (endRes >= getVisibleAlignmentWidth() - 1)
464       {
465         return false;
466       }
467
468       setStartRes(startRes + 1);
469     }
470
471     return true;
472   }
473
474   /**
475    * Scroll a wrapped alignment so that the specified residue is in the first
476    * repeat of the wrapped view. Fires a property change event. Answers true if
477    * the startRes changed, else false.
478    * 
479    * @param res
480    *          residue position to scroll to NB visible position not absolute
481    *          alignment position
482    * @return
483    */
484   public boolean scrollToWrappedVisible(int res)
485   {
486     int oldStartRes = startRes;
487     int width = getViewportWidth();
488
489     if (res >= oldStartRes && res < oldStartRes + width)
490     {
491       return false;
492     }
493
494     boolean up = res < oldStartRes;
495     int widthsToScroll = Math.abs((res - oldStartRes) / width);
496     if (up)
497     {
498       widthsToScroll++;
499     }
500
501     int residuesToScroll = width * widthsToScroll;
502     int newStartRes = up ? oldStartRes - residuesToScroll : oldStartRes
503             + residuesToScroll;
504     if (newStartRes < 0)
505     {
506       newStartRes = 0;
507     }
508
509     setStartRes(newStartRes);
510
511     return true;
512   }
513
514   /**
515    * Scroll so that (x,y) is visible. Fires a property change event.
516    * 
517    * @param x
518    *          x position in alignment (absolute position)
519    * @param y
520    *          y position in alignment (absolute position)
521    */
522   public void scrollToVisible(int x, int y)
523   {
524     while (y < startSeq)
525     {
526       scrollUp(true);
527     }
528     while (y > endSeq)
529     {
530       scrollUp(false);
531     }
532
533     HiddenColumns hidden = al.getHiddenColumns();
534     while (x < hidden.adjustForHiddenColumns(startRes))
535     {
536       if (!scrollRight(false))
537       {
538         break;
539       }
540     }
541     while (x > hidden.adjustForHiddenColumns(endRes))
542     {
543       if (!scrollRight(true))
544       {
545         break;
546       }
547     }
548   }
549
550   /**
551    * Adjust sequence position for page up. Fires a property change event.
552    */
553   public void pageUp()
554   {
555     if (wrappedMode)
556     {
557       setStartRes(Math.max(0, getStartRes() - getViewportWidth()));
558     }
559     else
560     {
561       setViewportStartAndHeight(startSeq - (endSeq - startSeq),
562               getViewportHeight());
563     }
564   }
565
566   /**
567    * Adjust sequence position for page down. Fires a property change event.
568    */
569   public void pageDown()
570   {
571     if (wrappedMode)
572     {
573       /*
574        * if height is more than width (i.e. not all sequences fit on screen),
575        * increase page down to height
576        */
577       int newStart = getStartRes()
578               + Math.max(getViewportHeight(), getViewportWidth());
579
580       /*
581        * don't page down beyond end of alignment, or if not all
582        * sequences fit in the visible height
583        */
584       if (newStart < getVisibleAlignmentWidth())
585       {
586         setStartRes(newStart);
587       }
588     }
589     else
590     {
591       setViewportStartAndHeight(endSeq, getViewportHeight());
592     }
593   }
594
595   public void setWrappedMode(boolean wrapped)
596   {
597     wrappedMode = wrapped;
598   }
599
600   public boolean isWrappedMode()
601   {
602     return wrappedMode;
603   }
604
605   /**
606    * Answers the vertical scroll position (0..) to set, given the visible column
607    * that is at top left.
608    * 
609    * <pre>
610    * Example:
611    *    viewport width 40 columns (0-39, 40-79, 80-119...)
612    *    column 0 returns scroll position 0
613    *    columns 1-40 return scroll position 1
614    *    columns 41-80 return scroll position 2
615    *    etc
616    * </pre>
617    * 
618    * @param topLeftColumn
619    *          (0..)
620    * @return
621    */
622   public int getWrappedScrollPosition(final int topLeftColumn)
623   {
624     int w = getViewportWidth();
625
626     /*
627      * visible whole widths
628      */
629     int scroll = topLeftColumn / w;
630
631     /*
632      * add 1 for a part width if there is one
633      */
634     scroll += topLeftColumn % w > 0 ? 1 : 0;
635
636     return scroll;
637   }
638
639   /**
640    * Answers the maximum wrapped vertical scroll value, given the column
641    * position (0..) to show at top left of the visible region.
642    * 
643    * @param topLeftColumn
644    * @return
645    */
646   public int getWrappedMaxScroll(int topLeftColumn)
647   {
648     int scrollPosition = getWrappedScrollPosition(topLeftColumn);
649
650     /*
651      * how many more widths could be drawn after this one?
652      */
653     int columnsRemaining = getVisibleAlignmentWidth() - topLeftColumn;
654     int width = getViewportWidth();
655     int widthsRemaining = columnsRemaining / width
656             + (columnsRemaining % width > 0 ? 1 : 0) - 1;
657     int maxScroll = scrollPosition + widthsRemaining;
658
659     return maxScroll;
660   }
661 }