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