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