First try, not finished
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpeclipse / phpeditor / OverviewRuler.java
1 /**********************************************************************
2 Copyright (c) 2000, 2002 IBM Corp. and others.
3 All rights reserved. This program and the accompanying materials
4 are made available under the terms of the Common Public License v1.0
5 which accompanies this distribution, and is available at
6 http://www.eclipse.org/legal/cpl-v10.html
7
8 Contributors:
9     IBM Corporation - Initial implementation
10 **********************************************************************/
11
12 package net.sourceforge.phpeclipse.phpeditor;
13
14 import java.util.ArrayList;
15 import java.util.Collections;
16 import java.util.HashMap;
17 import java.util.HashSet;
18 import java.util.Iterator;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Set;
22
23 import org.eclipse.jface.text.BadLocationException;
24 import org.eclipse.jface.text.IDocument;
25 import org.eclipse.jface.text.IRegion;
26 import org.eclipse.jface.text.ITextListener;
27 import org.eclipse.jface.text.ITextViewer;
28 import org.eclipse.jface.text.ITextViewerExtension3;
29 import org.eclipse.jface.text.Position;
30 import org.eclipse.jface.text.Region;
31 import org.eclipse.jface.text.TextEvent;
32 import org.eclipse.jface.text.source.Annotation;
33 import org.eclipse.jface.text.source.IAnnotationModel;
34 import org.eclipse.jface.text.source.IAnnotationModelListener;
35 import org.eclipse.jface.text.source.IVerticalRulerInfo;
36 import org.eclipse.swt.SWT;
37 import org.eclipse.swt.custom.StyledText;
38 import org.eclipse.swt.events.DisposeEvent;
39 import org.eclipse.swt.events.DisposeListener;
40 import org.eclipse.swt.events.MouseAdapter;
41 import org.eclipse.swt.events.MouseEvent;
42 import org.eclipse.swt.events.MouseMoveListener;
43 import org.eclipse.swt.events.PaintEvent;
44 import org.eclipse.swt.events.PaintListener;
45 import org.eclipse.swt.graphics.Color;
46 import org.eclipse.swt.graphics.Cursor;
47 import org.eclipse.swt.graphics.GC;
48 import org.eclipse.swt.graphics.Image;
49 import org.eclipse.swt.graphics.Point;
50 import org.eclipse.swt.graphics.RGB;
51 import org.eclipse.swt.graphics.Rectangle;
52 import org.eclipse.swt.widgets.Canvas;
53 import org.eclipse.swt.widgets.Composite;
54 import org.eclipse.swt.widgets.Control;
55 import org.eclipse.swt.widgets.Display;
56
57 /**
58  * 
59  */
60 public class OverviewRuler implements IVerticalRulerInfo {
61
62   /**
63    * Internal listener class.
64    */
65   class InternalListener implements ITextListener, IAnnotationModelListener {
66
67     /*
68      * @see ITextListener#textChanged
69      */
70     public void textChanged(TextEvent e) {
71       if (fTextViewer != null
72         && e.getDocumentEvent() == null
73         && e.getViewerRedrawState()) {
74         // handle only changes of visible document
75         redraw();
76       }
77     }
78
79     /*
80      * @see IAnnotationModelListener#modelChanged(IAnnotationModel)
81      */
82     public void modelChanged(IAnnotationModel model) {
83       update();
84     }
85   }
86
87   /**
88    * Filters problems based on their types.
89    */
90   class FilterIterator implements Iterator {
91
92     private final static int IGNORE = 0;
93     private final static int TEMPORARY = 1;
94     private final static int PERSISTENT = 2;
95
96     private Iterator fIterator;
97     private AnnotationType fType;
98     private IProblemAnnotation fNext;
99     private int fTemporary;
100
101     public FilterIterator(AnnotationType type) {
102       this(type, IGNORE);
103     }
104
105     public FilterIterator(AnnotationType type, boolean temporary) {
106       this(type, temporary ? TEMPORARY : PERSISTENT);
107     }
108
109     private FilterIterator(AnnotationType type, int temporary) {
110       fType = type;
111       fTemporary = temporary;
112       if (fModel != null) {
113         fIterator = fModel.getAnnotationIterator();
114         skip();
115       }
116     }
117
118     private void skip() {
119       while (fIterator.hasNext()) {
120         Object next = fIterator.next();
121         if (next instanceof IProblemAnnotation) {
122           fNext = (IProblemAnnotation) next;
123           AnnotationType type = fNext.getAnnotationType();
124           if (fType == AnnotationType.ALL || fType == type) {
125             if (fTemporary == IGNORE)
126               return;
127             if (fTemporary == TEMPORARY && fNext.isTemporary())
128               return;
129             if (fTemporary == PERSISTENT && !fNext.isTemporary())
130               return;
131           }
132         }
133       }
134       fNext = null;
135     }
136
137     /*
138      * @see Iterator#hasNext()
139      */
140     public boolean hasNext() {
141       return fNext != null;
142     }
143     /*
144      * @see Iterator#next()
145      */
146     public Object next() {
147       try {
148         return fNext;
149       } finally {
150         if (fModel != null)
151           skip();
152       }
153     }
154     /*
155      * @see Iterator#remove()
156      */
157     public void remove() {
158       throw new UnsupportedOperationException();
159     }
160   };
161
162   private static final int INSET = 2;
163   private static final int PROBLEM_HEIGHT_MIN = 4;
164   private static boolean PROBLEM_HEIGHT_SCALABLE = false;
165
166   /** The model of the overview ruler */
167   private IAnnotationModel fModel;
168   /** The view to which this ruler is connected */
169   private ITextViewer fTextViewer;
170   /** The ruler's canvas */
171   private Canvas fCanvas;
172   /** The drawable for double buffering */
173   private Image fBuffer;
174   /** The internal listener */
175   private InternalListener fInternalListener = new InternalListener();
176   /** The width of this vertical ruler */
177   private int fWidth;
178   /** The hit detection cursor */
179   private Cursor fHitDetectionCursor;
180   /** The last cursor */
181   private Cursor fLastCursor;
182   /** Cache for the actual scroll position in pixels */
183   private int fScrollPos;
184   /** The line of the last mouse button activity */
185   private int fLastMouseButtonActivityLine = -1;
186   /** The actual problem height */
187   private int fProblemHeight = -1;
188
189   private Set fAnnotationSet = new HashSet();
190   private Map fLayers = new HashMap();
191   private Map fColorTable = new HashMap();
192
193   /**
194    * Constructs a vertical ruler with the given width.
195    *
196    * @param width the width of the vertical ruler
197    */
198   public OverviewRuler(int width) {
199     fWidth = width;
200   }
201
202   public Control getControl() {
203     return fCanvas;
204   }
205
206   public int getWidth() {
207     return fWidth;
208   }
209
210   public void setModel(IAnnotationModel model) {
211     if (model != fModel || model != null) {
212
213       if (fModel != null)
214         fModel.removeAnnotationModelListener(fInternalListener);
215
216       fModel = model;
217
218       if (fModel != null)
219         fModel.addAnnotationModelListener(fInternalListener);
220
221       update();
222     }
223   }
224
225   public Control createControl(Composite parent, ITextViewer textViewer) {
226
227     fTextViewer = textViewer;
228
229     fHitDetectionCursor = new Cursor(parent.getDisplay(), SWT.CURSOR_HAND);
230     fCanvas = new Canvas(parent, SWT.NO_BACKGROUND);
231
232     fCanvas.addPaintListener(new PaintListener() {
233       public void paintControl(PaintEvent event) {
234         if (fTextViewer != null)
235           doubleBufferPaint(event.gc);
236       }
237     });
238
239     fCanvas.addDisposeListener(new DisposeListener() {
240       public void widgetDisposed(DisposeEvent event) {
241         handleDispose();
242         fTextViewer = null;
243       }
244     });
245
246     fCanvas.addMouseListener(new MouseAdapter() {
247       public void mouseDown(MouseEvent event) {
248         handleMouseDown(event);
249       }
250     });
251
252     fCanvas.addMouseMoveListener(new MouseMoveListener() {
253       public void mouseMove(MouseEvent event) {
254         handleMouseMove(event);
255       }
256     });
257
258     if (fTextViewer != null)
259       fTextViewer.addTextListener(fInternalListener);
260
261     return fCanvas;
262   }
263
264   /**
265    * Disposes the ruler's resources.
266    */
267   private void handleDispose() {
268
269     if (fTextViewer != null) {
270       fTextViewer.removeTextListener(fInternalListener);
271       fTextViewer = null;
272     }
273
274     if (fModel != null)
275       fModel.removeAnnotationModelListener(fInternalListener);
276
277     if (fBuffer != null) {
278       fBuffer.dispose();
279       fBuffer = null;
280     }
281
282     if (fHitDetectionCursor != null) {
283       fHitDetectionCursor.dispose();
284       fHitDetectionCursor = null;
285     }
286
287     fAnnotationSet.clear();
288     fLayers.clear();
289     fColorTable.clear();
290   }
291
292   /**
293    * Double buffer drawing.
294    */
295   private void doubleBufferPaint(GC dest) {
296
297     Point size = fCanvas.getSize();
298
299     if (size.x <= 0 || size.y <= 0)
300       return;
301
302     if (fBuffer != null) {
303       Rectangle r = fBuffer.getBounds();
304       if (r.width != size.x || r.height != size.y) {
305         fBuffer.dispose();
306         fBuffer = null;
307       }
308     }
309     if (fBuffer == null)
310       fBuffer = new Image(fCanvas.getDisplay(), size.x, size.y);
311
312     GC gc = new GC(fBuffer);
313     try {
314       gc.setBackground(fCanvas.getBackground());
315       gc.fillRectangle(0, 0, size.x, size.y);
316
317       if (fTextViewer instanceof ITextViewerExtension3)
318         doPaint1(gc);
319       else
320         doPaint(gc);
321
322     } finally {
323       gc.dispose();
324     }
325
326     dest.drawImage(fBuffer, 0, 0);
327   }
328
329   private void doPaint(GC gc) {
330
331     if (fTextViewer == null)
332       return;
333
334     Rectangle r = new Rectangle(0, 0, 0, 0);
335     int yy, hh = PROBLEM_HEIGHT_MIN;
336
337     IDocument document = fTextViewer.getDocument();
338     IRegion visible = fTextViewer.getVisibleRegion();
339
340     StyledText textWidget = fTextViewer.getTextWidget();
341     int maxLines = textWidget.getLineCount();
342     fScrollPos = textWidget.getTopPixel();
343
344     Point size = fCanvas.getSize();
345     int writable = maxLines * textWidget.getLineHeight();
346     if (size.y > writable)
347       size.y = writable;
348
349     List indices = new ArrayList(fLayers.keySet());
350     Collections.sort(indices);
351
352     for (Iterator iterator = indices.iterator(); iterator.hasNext();) {
353       Object layer = iterator.next();
354       AnnotationType annotationType = (AnnotationType) fLayers.get(layer);
355
356       if (skip(annotationType))
357         continue;
358
359       boolean[] temporary = new boolean[] { false, true };
360       for (int t = 0; t < temporary.length; t++) {
361
362         Iterator e = new FilterIterator(annotationType, temporary[t]);
363         Color fill = getFillColor(annotationType, temporary[t]);
364         Color stroke = getStrokeColor(annotationType, temporary[t]);
365
366         for (int i = 0; e.hasNext(); i++) {
367
368           Annotation a = (Annotation) e.next();
369           Position p = fModel.getPosition(a);
370
371           if (p == null
372             || !p.overlapsWith(visible.getOffset(), visible.getLength()))
373             continue;
374
375           int problemOffset = Math.max(p.getOffset(), visible.getOffset());
376           int problemEnd =
377             Math.min(
378               p.getOffset() + p.getLength(),
379               visible.getOffset() + visible.getLength());
380           int problemLength = problemEnd - problemOffset;
381
382           try {
383             if (PROBLEM_HEIGHT_SCALABLE) {
384               int numbersOfLines =
385                 document.getNumberOfLines(problemOffset, problemLength);
386               hh = (numbersOfLines * size.y) / maxLines;
387               if (hh < PROBLEM_HEIGHT_MIN)
388                 hh = PROBLEM_HEIGHT_MIN;
389             }
390             fProblemHeight = hh;
391
392             int startLine =
393               textWidget.getLineAtOffset(problemOffset - visible.getOffset());
394             yy = Math.min((startLine * size.y) / maxLines, size.y - hh);
395
396             if (fill != null) {
397               gc.setBackground(fill);
398               gc.fillRectangle(INSET, yy, size.x - (2 * INSET), hh);
399             }
400
401             if (stroke != null) {
402               gc.setForeground(stroke);
403               r.x = INSET;
404               r.y = yy;
405               r.width = size.x - (2 * INSET) - 1;
406               r.height = hh;
407               gc.setLineWidth(1);
408               gc.drawRectangle(r);
409             }
410           } catch (BadLocationException x) {
411           }
412         }
413       }
414     }
415   }
416
417   private void doPaint1(GC gc) {
418
419     if (fTextViewer == null)
420       return;
421
422     Rectangle r = new Rectangle(0, 0, 0, 0);
423     int yy, hh = PROBLEM_HEIGHT_MIN;
424
425     ITextViewerExtension3 extension = (ITextViewerExtension3) fTextViewer;
426     IDocument document = fTextViewer.getDocument();
427     StyledText textWidget = fTextViewer.getTextWidget();
428     fScrollPos = textWidget.getTopPixel();
429
430     int maxLines = textWidget.getLineCount();
431     Point size = fCanvas.getSize();
432     int writable = maxLines * textWidget.getLineHeight();
433     if (size.y > writable)
434       size.y = writable;
435
436     List indices = new ArrayList(fLayers.keySet());
437     Collections.sort(indices);
438
439     for (Iterator iterator = indices.iterator(); iterator.hasNext();) {
440       Object layer = iterator.next();
441       AnnotationType annotationType = (AnnotationType) fLayers.get(layer);
442
443       if (skip(annotationType))
444         continue;
445
446       boolean[] temporary = new boolean[] { false, true };
447       for (int t = 0; t < temporary.length; t++) {
448
449         Iterator e = new FilterIterator(annotationType, temporary[t]);
450         Color fill = getFillColor(annotationType, temporary[t]);
451         Color stroke = getStrokeColor(annotationType, temporary[t]);
452
453         for (int i = 0; e.hasNext(); i++) {
454
455           Annotation a = (Annotation) e.next();
456           Position p = fModel.getPosition(a);
457
458           if (p == null)
459             continue;
460
461           IRegion widgetRegion =
462             extension.modelRange2WidgetRange(
463               new Region(p.getOffset(), p.getLength()));
464           if (widgetRegion == null)
465             continue;
466
467           try {
468             if (PROBLEM_HEIGHT_SCALABLE) {
469               int numbersOfLines =
470                 document.getNumberOfLines(p.getOffset(), p.getLength());
471               hh = (numbersOfLines * size.y) / maxLines;
472               if (hh < PROBLEM_HEIGHT_MIN)
473                 hh = PROBLEM_HEIGHT_MIN;
474             }
475             fProblemHeight = hh;
476
477             int startLine =
478               textWidget.getLineAtOffset(widgetRegion.getOffset());
479             yy = Math.min((startLine * size.y) / maxLines, size.y - hh);
480
481             if (fill != null) {
482               gc.setBackground(fill);
483               gc.fillRectangle(INSET, yy, size.x - (2 * INSET), hh);
484             }
485
486             if (stroke != null) {
487               gc.setForeground(stroke);
488               r.x = INSET;
489               r.y = yy;
490               r.width = size.x - (2 * INSET) - 1;
491               r.height = hh;
492               gc.setLineWidth(1);
493               gc.drawRectangle(r);
494             }
495           } catch (BadLocationException x) {
496           }
497         }
498       }
499     }
500   }
501
502   /**
503    * Thread-safe implementation.
504    * Can be called from any thread.
505    */
506   public void update() {
507     if (fCanvas != null && !fCanvas.isDisposed()) {
508       Display d = fCanvas.getDisplay();
509       if (d != null) {
510         d.asyncExec(new Runnable() {
511           public void run() {
512             redraw();
513           }
514         });
515       }
516     }
517   }
518
519   /**
520    * Redraws the overview ruler.
521    */
522   private void redraw() {
523     if (fCanvas != null && !fCanvas.isDisposed()) {
524       GC gc = new GC(fCanvas);
525       doubleBufferPaint(gc);
526       gc.dispose();
527     }
528   }
529
530   private int[] toLineNumbers(int y_coordinate) {
531
532     StyledText textWidget = fTextViewer.getTextWidget();
533     int maxLines = textWidget.getContent().getLineCount();
534
535     int rulerLength = fCanvas.getSize().y;
536     int writable = maxLines * textWidget.getLineHeight();
537
538     if (rulerLength > writable)
539       rulerLength = writable;
540
541     if (y_coordinate >= writable)
542       return new int[] { -1, -1 };
543
544     int[] lines = new int[2];
545
546     int pixel = Math.max(y_coordinate - 1, 0);
547     lines[0] = (pixel * maxLines) / rulerLength;
548
549     pixel = Math.min(rulerLength, y_coordinate + 1);
550     lines[1] = (pixel * maxLines) / rulerLength;
551
552     if (fTextViewer instanceof ITextViewerExtension3) {
553       ITextViewerExtension3 extension = (ITextViewerExtension3) fTextViewer;
554       lines[0] = extension.widgetlLine2ModelLine(lines[0]);
555       lines[1] = extension.widgetlLine2ModelLine(lines[1]);
556     } else {
557       try {
558         IRegion visible = fTextViewer.getVisibleRegion();
559         int lineNumber =
560           fTextViewer.getDocument().getLineOfOffset(visible.getOffset());
561         lines[0] += lineNumber;
562         lines[1] += lineNumber;
563       } catch (BadLocationException x) {
564       }
565     }
566
567     return lines;
568   }
569
570   boolean hasAnnotationAt(int y_coordinate) {
571     return findBestMatchingLineNumber(toLineNumbers(y_coordinate)) != -1;
572   }
573
574   private Position getProblemPositionAt(int[] lineNumbers) {
575     if (lineNumbers[0] == -1)
576       return null;
577
578     Position found = null;
579
580     try {
581       IDocument d = fTextViewer.getDocument();
582       IRegion line = d.getLineInformation(lineNumbers[0]);
583
584       int start = line.getOffset();
585
586       line = d.getLineInformation(lineNumbers[lineNumbers.length - 1]);
587       int end = line.getOffset() + line.getLength();
588
589       Iterator e = new FilterIterator(AnnotationType.ALL);
590       while (e.hasNext()) {
591         Annotation a = (Annotation) e.next();
592         Position p = fModel.getPosition(a);
593         if (start <= p.getOffset() && p.getOffset() < end) {
594           if (found == null || p.getOffset() < found.getOffset())
595             found = p;
596         }
597       }
598
599     } catch (BadLocationException x) {
600     }
601
602     return found;
603   }
604
605   /**
606    * Returns the line which best corresponds to one of
607    * the underlying problem annotations at the given
608    * y ruler coordinate.
609    * 
610    * @return the best matching line or <code>-1</code> if no such line can be found
611    * @since 2.1
612    */
613   private int findBestMatchingLineNumber(int[] lineNumbers) {
614     if (lineNumbers == null || lineNumbers.length < 1)
615       return -1;
616
617     try {
618       Position pos = getProblemPositionAt(lineNumbers);
619       if (pos == null)
620         return -1;
621       return fTextViewer.getDocument().getLineOfOffset(pos.getOffset());
622     } catch (BadLocationException ex) {
623       return -1;
624     }
625   }
626
627   private void handleMouseDown(MouseEvent event) {
628     if (fTextViewer != null) {
629       int[] lines = toLineNumbers(event.y);
630       Position p = getProblemPositionAt(lines);
631       if (p != null) {
632         fTextViewer.revealRange(p.getOffset(), p.getLength());
633         fTextViewer.setSelectedRange(p.getOffset(), p.getLength());
634       }
635       fTextViewer.getTextWidget().setFocus();
636     }
637     fLastMouseButtonActivityLine = toDocumentLineNumber(event.y);
638   }
639
640   private void handleMouseMove(MouseEvent event) {
641     if (fTextViewer != null) {
642       int[] lines = toLineNumbers(event.y);
643       Position p = getProblemPositionAt(lines);
644       Cursor cursor = (p != null ? fHitDetectionCursor : null);
645       if (cursor != fLastCursor) {
646         fCanvas.setCursor(cursor);
647         fLastCursor = cursor;
648       }
649     }
650   }
651
652   private void handleMouseDoubleClick(MouseEvent event) {
653     fLastMouseButtonActivityLine = toDocumentLineNumber(event.y);
654   }
655
656   public void showAnnotation(AnnotationType annotationType, boolean show) {
657     if (show)
658       fAnnotationSet.add(annotationType);
659     else
660       fAnnotationSet.remove(annotationType);
661   }
662
663   public void setLayer(AnnotationType annotationType, int layer) {
664     if (layer >= 0)
665       fLayers.put(new Integer(layer), annotationType);
666     else {
667       Iterator e = fLayers.keySet().iterator();
668       while (e.hasNext()) {
669         Object key = e.next();
670         if (annotationType.equals(fLayers.get(key))) {
671           fLayers.remove(key);
672           return;
673         }
674       }
675     }
676   }
677
678   public void setColor(AnnotationType annotationType, Color color) {
679     if (color != null)
680       fColorTable.put(annotationType, color);
681     else
682       fColorTable.remove(annotationType);
683   }
684
685   private boolean skip(AnnotationType annotationType) {
686     return !fAnnotationSet.contains(annotationType);
687   }
688
689   private static RGB interpolate(RGB fg, RGB bg, double scale) {
690     return new RGB(
691       (int) ((1.0 - scale) * fg.red + scale * bg.red),
692       (int) ((1.0 - scale) * fg.green + scale * bg.green),
693       (int) ((1.0 - scale) * fg.blue + scale * bg.blue));
694   }
695
696   private static double greyLevel(RGB rgb) {
697     if (rgb.red == rgb.green && rgb.green == rgb.blue)
698       return rgb.red;
699     return (0.299 * rgb.red + 0.587 * rgb.green + 0.114 * rgb.blue + 0.5);
700   }
701
702   private static boolean isDark(RGB rgb) {
703     return greyLevel(rgb) > 128;
704   }
705
706   private static Color getColor(RGB rgb) {
707     //          PHPTextTools textTools= PHPeclipsePlugin.getDefault().getPHPTextTools();
708     //          return textTools.getColorManager().getColor(rgb);
709     return PHPEditorEnvironment.getPHPColorProvider().getColor(rgb);
710   }
711
712   private Color getColor(AnnotationType annotationType, double scale) {
713     Color base = (Color) fColorTable.get(annotationType);
714     if (base == null)
715       return null;
716
717     RGB baseRGB = base.getRGB();
718     RGB background = fCanvas.getBackground().getRGB();
719
720     boolean darkBase = isDark(baseRGB);
721     boolean darkBackground = isDark(background);
722     if (darkBase && darkBackground)
723       background = new RGB(255, 255, 255);
724     else if (!darkBase && !darkBackground)
725       background = new RGB(0, 0, 0);
726
727     return getColor(interpolate(baseRGB, background, scale));
728   }
729
730   private Color getStrokeColor(
731     AnnotationType annotationType,
732     boolean temporary) {
733     return getColor(annotationType, temporary ? 0.5 : 0.2);
734   }
735
736   private Color getFillColor(
737     AnnotationType annotationType,
738     boolean temporary) {
739     return getColor(annotationType, temporary ? 0.9 : 0.6);
740   }
741
742   /**
743    * @see IVerticalRulerInfo#getLineOfLastMouseButtonActivity()
744    * @since 2.1
745    */
746   public int getLineOfLastMouseButtonActivity() {
747     return fLastMouseButtonActivityLine;
748   }
749
750   /**
751    * @see IVerticalRulerInfo#toDocumentLineNumber(int)
752    * @since 2.1
753    */
754   public int toDocumentLineNumber(int y_coordinate) {
755
756     if (fTextViewer == null || y_coordinate == -1)
757       return -1;
758
759     int[] lineNumbers = toLineNumbers(y_coordinate);
760     int bestLine = findBestMatchingLineNumber(lineNumbers);
761     if (bestLine == -1 && lineNumbers.length > 0)
762       return lineNumbers[0];
763     return bestLine;
764   }
765
766   /**
767    * Returns the height of the problem rectangle.
768    * 
769    * @return the height of the problem rectangle
770    * @since 2.1
771    */
772   int getAnnotationHeight() {
773     return fProblemHeight;
774   }
775 }