d02d6749664f1d12b9d3964894cbf166edb1378f
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / ui / text / link / LinkedPositionUI.java
1 /*
2  * (c) Copyright IBM Corp. 2000, 2001.
3  * All Rights Reserved.
4  */
5 package net.sourceforge.phpdt.internal.ui.text.link;
6
7 import java.lang.reflect.InvocationTargetException;
8
9 import net.sourceforge.phpdt.internal.ui.util.ExceptionHandler;
10 import net.sourceforge.phpdt.ui.PreferenceConstants;
11 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
12
13 import org.eclipse.core.runtime.CoreException;
14 import org.eclipse.jface.dialogs.MessageDialog;
15 import org.eclipse.jface.preference.IPreferenceStore;
16 import org.eclipse.jface.preference.PreferenceConverter;
17 import org.eclipse.jface.text.BadLocationException;
18 import org.eclipse.jface.text.BadPositionCategoryException;
19 import org.eclipse.jface.text.DefaultPositionUpdater;
20 import org.eclipse.jface.text.IDocument;
21 import org.eclipse.jface.text.IPositionUpdater;
22 import org.eclipse.jface.text.IRegion;
23 import org.eclipse.jface.text.ITextInputListener;
24 import org.eclipse.jface.text.ITextListener;
25 import org.eclipse.jface.text.ITextViewer;
26 import org.eclipse.jface.text.ITextViewerExtension;
27 import org.eclipse.jface.text.ITextViewerExtension2;
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.util.Assert;
33 import org.eclipse.jface.util.IPropertyChangeListener;
34 import org.eclipse.jface.util.PropertyChangeEvent;
35 import org.eclipse.swt.SWT;
36 import org.eclipse.swt.custom.StyledText;
37 import org.eclipse.swt.custom.VerifyKeyListener;
38 import org.eclipse.swt.events.ModifyEvent;
39 import org.eclipse.swt.events.ModifyListener;
40 import org.eclipse.swt.events.PaintEvent;
41 import org.eclipse.swt.events.PaintListener;
42 import org.eclipse.swt.events.ShellEvent;
43 import org.eclipse.swt.events.ShellListener;
44 import org.eclipse.swt.events.VerifyEvent;
45 import org.eclipse.swt.events.VerifyListener;
46 import org.eclipse.swt.graphics.Color;
47 import org.eclipse.swt.graphics.GC;
48 import org.eclipse.swt.graphics.Point;
49 import org.eclipse.swt.graphics.RGB;
50 import org.eclipse.swt.widgets.Display;
51 import org.eclipse.swt.widgets.Shell;
52
53 /**
54  * A user interface for <code>LinkedPositionManager</code>, using <code>ITextViewer</code>.
55  */
56 public class LinkedPositionUI implements LinkedPositionListener,
57         ITextInputListener, ITextListener, ModifyListener, VerifyListener, VerifyKeyListener, PaintListener, IPropertyChangeListener, ShellListener {
58
59         /**
60          * A listener for notification when the user cancelled the edit operation.
61          */
62         public interface ExitListener {
63                 void exit(boolean accept);
64         }
65         
66         public static class ExitFlags {
67                 public int flags;       
68                 public boolean doit;
69                 public ExitFlags(int flags, boolean doit) {
70                         this.flags= flags;
71                         this.doit= doit;
72                 }                                               
73         }
74         
75         public interface ExitPolicy {
76                 ExitFlags doExit(LinkedPositionManager manager, VerifyEvent event, int offset, int length);
77         }
78         
79         // leave flags
80         private static final int UNINSTALL= 1;                  // uninstall linked position manager
81         public static final int COMMIT= 2;                              // commit changes
82         private static final int DOCUMENT_CHANGED= 4;   // document has changed
83         public static final int UPDATE_CARET= 8;                // update caret
84
85         private static final String CARET_POSITION= "LinkedPositionUI.caret.position"; //$NON-NLS-1$
86         private static final IPositionUpdater fgUpdater= new DefaultPositionUpdater(CARET_POSITION);
87         private static final IPreferenceStore fgStore= PHPeclipsePlugin.getDefault().getPreferenceStore();
88         
89         private final ITextViewer fViewer;
90         private final LinkedPositionManager fManager;   
91         private Color fFrameColor;
92
93         private int fFinalCaretOffset= -1; // no final caret offset
94
95         private Position fFramePosition;
96         private int fInitialOffset= -1;
97         private int fCaretOffset;
98         
99         private ExitPolicy fExitPolicy;
100         private ExitListener fExitListener;
101         
102         private boolean fNeedRedraw;
103         
104         private String fContentType;
105
106         /**
107          * Creates a user interface for <code>LinkedPositionManager</code>.
108          * 
109          * @param viewer  the text viewer.
110          * @param manager the <code>LinkedPositionManager</code> managing a <code>IDocument</code> of the <code>ITextViewer</code>.
111          */
112         public LinkedPositionUI(ITextViewer viewer, LinkedPositionManager manager) {
113                 Assert.isNotNull(viewer);
114                 Assert.isNotNull(manager);
115                 
116                 fViewer= viewer;
117                 fManager= manager;
118                 
119                 fManager.setLinkedPositionListener(this);
120
121                 initializeHighlightColor(viewer);
122         }
123
124         /*
125          * @see IPropertyChangeListener#propertyChange(PropertyChangeEvent)
126          */
127         public void propertyChange(PropertyChangeEvent event) {
128                 if (event.getProperty().equals(PreferenceConstants.EDITOR_LINKED_POSITION_COLOR)) {
129                         initializeHighlightColor(fViewer);
130                         redrawRegion();
131                 }
132         }
133
134         private void initializeHighlightColor(ITextViewer viewer) {
135
136                 if (fFrameColor != null)
137                         fFrameColor.dispose();
138
139                 StyledText text= viewer.getTextWidget();
140                 if (text != null) {
141                         Display display= text.getDisplay();
142                         fFrameColor= createColor(fgStore, PreferenceConstants.EDITOR_LINKED_POSITION_COLOR, display);
143                 }
144         }
145
146         /**
147          * Creates a color from the information stored in the given preference store.
148          * Returns <code>null</code> if there is no such information available.
149          */
150         private Color createColor(IPreferenceStore store, String key, Display display) {
151         
152                 RGB rgb= null;          
153                 
154                 if (store.contains(key)) {
155                         
156                         if (store.isDefault(key))
157                                 rgb= PreferenceConverter.getDefaultColor(store, key);
158                         else
159                                 rgb= PreferenceConverter.getColor(store, key);
160                 
161                         if (rgb != null)
162                                 return new Color(display, rgb);
163                 }
164                 
165                 return null;
166         }
167
168         /**
169          * Sets the initial offset.
170          * @param offset
171          */
172         public void setInitialOffset(int offset) {
173                 fInitialOffset= offset; 
174         }
175         
176         /**
177          * Sets the final position of the caret when the linked mode is exited
178          * successfully by leaving the last linked position using TAB.
179          */
180         public void setFinalCaretOffset(int offset) {
181                 fFinalCaretOffset= offset;      
182         }
183
184         /**
185          * Sets a <code>CancelListener</code> which is notified if the linked mode
186          * is exited unsuccessfully by hitting ESC.
187          */
188         public void setCancelListener(ExitListener listener) {
189                 fExitListener= listener;
190         }
191
192         /**
193          * Sets an <code>ExitPolicy</code> which decides when and how
194          * the linked mode is exited.
195          */
196         public void setExitPolicy(ExitPolicy policy) {
197                 fExitPolicy= policy;
198         }
199
200         /*
201          * @see LinkedPositionManager.LinkedPositionListener#setCurrentPositions(Position, int)
202          */
203         public void setCurrentPosition(Position position, int caretOffset) {
204                 if (!fFramePosition.equals(position)) {
205                         fNeedRedraw= true;
206                         fFramePosition= position;
207                 }
208
209                 fCaretOffset= caretOffset;
210         }
211
212         /**
213          * Enters the linked mode. The linked mode can be left by calling
214          * <code>exit</code>.
215          * 
216          * @see #exit(boolean)
217          */
218         public void enter() {
219
220                 // track final caret
221                 IDocument document= fViewer.getDocument();
222                 document.addPositionCategory(CARET_POSITION);
223                 document.addPositionUpdater(fgUpdater);
224
225                 try {
226                         if (fFinalCaretOffset != -1)
227                                 document.addPosition(CARET_POSITION, new Position(fFinalCaretOffset));
228                 } catch (BadLocationException e) {
229                         handleException(fViewer.getTextWidget().getShell(), e);
230
231                 } catch (BadPositionCategoryException e) {
232       PHPeclipsePlugin.log(e);
233                         Assert.isTrue(false);
234                 }
235
236                 fViewer.addTextInputListener(this);
237                 fViewer.addTextListener(this);
238                                 
239                 ITextViewerExtension extension= (ITextViewerExtension) fViewer;
240                 extension.prependVerifyKeyListener(this);
241
242                 StyledText text= fViewer.getTextWidget();                       
243                 text.addVerifyListener(this);
244                 text.addModifyListener(this);
245                 text.addPaintListener(this);
246                 text.showSelection();
247
248                 Shell shell= text.getShell();
249                 shell.addShellListener(this);
250
251                 fFramePosition= (fInitialOffset == -1) ? fManager.getFirstPosition() : fManager.getPosition(fInitialOffset);
252                 if (fFramePosition == null) {
253                         leave(UNINSTALL | COMMIT | UPDATE_CARET);
254                         return;
255                 }
256
257                 fgStore.addPropertyChangeListener(this);
258
259                 try {
260                         fContentType= document.getContentType(fFramePosition.offset);
261                         if (fViewer instanceof ITextViewerExtension2) {
262                                 ((ITextViewerExtension2) fViewer).prependAutoEditStrategy(fManager, fContentType);
263                         } else {
264                                 Assert.isTrue(false);
265                         }
266
267                 } catch (BadLocationException e) {
268                         handleException(fViewer.getTextWidget().getShell(), e);
269                 }
270         }
271
272         /*
273          * @see LinkedPositionManager.LinkedPositionListener#exit(boolean)
274          */
275         public void exit(boolean success) {
276                 // no UNINSTALL since manager has already uninstalled itself
277                 leave((success ? COMMIT : 0) | UPDATE_CARET);
278         }
279
280         /**
281          * Returns the cursor selection, after having entered the linked mode.
282          * <code>enter()</code> must be called prior to a call to this method.
283          */
284         public IRegion getSelectedRegion() {
285                 if (fFramePosition == null)
286                         return new Region(fFinalCaretOffset, 0);
287                 else
288                         return new Region(fFramePosition.getOffset(), fFramePosition.getLength());
289         }
290         
291         private void leave(int flags) {
292
293                 fInitialOffset= -1;
294                 
295                 if ((flags & UNINSTALL) != 0)
296                         fManager.uninstall((flags & COMMIT) != 0);
297
298                 fgStore.removePropertyChangeListener(this);
299                 
300                 if (fFrameColor != null) {
301                         fFrameColor.dispose();
302                         fFrameColor= null;
303                 }                       
304                 
305                 StyledText text= fViewer.getTextWidget();       
306                 text.removePaintListener(this);
307                 text.removeModifyListener(this);
308                 text.removeVerifyListener(this);
309
310                 Shell shell= text.getShell();
311                 shell.removeShellListener(this);
312
313                 ITextViewerExtension extension= (ITextViewerExtension) fViewer;
314                 extension.removeVerifyKeyListener(this);
315
316                 if (fViewer instanceof ITextViewerExtension2 && fContentType != null)
317                         ((ITextViewerExtension2) fViewer).removeAutoEditStrategy(fManager, fContentType);
318                 fContentType= null;
319
320                 fViewer.removeTextListener(this);
321                 fViewer.removeTextInputListener(this);
322                 
323                 try {
324                         IDocument document= fViewer.getDocument();
325
326                         if (((flags & COMMIT) != 0) &&
327                                 ((flags & DOCUMENT_CHANGED) == 0) &&
328                                 ((flags & UPDATE_CARET) != 0))
329                         {
330                                 Position[] positions= document.getPositions(CARET_POSITION);
331                                 if ((positions != null) && (positions.length != 0)) {
332                                         
333                                         if (fViewer instanceof ITextViewerExtension3) {
334                                                 ITextViewerExtension3 extension3= (ITextViewerExtension3) fViewer;
335                                                 int widgetOffset= extension3.modelOffset2WidgetOffset(positions[0].getOffset());
336                                                 if (widgetOffset >= 0)
337                                                         text.setSelection(widgetOffset, widgetOffset);
338                                                         
339                                         } else {
340                                                 IRegion region= fViewer.getVisibleRegion();
341                                                 int offset= positions[0].getOffset() - region.getOffset();
342                                                 if ((offset >= 0) && (offset <= region.getLength()))
343                                                         text.setSelection(offset, offset);
344                                         }
345                                 }
346                         }
347
348                         document.removePositionUpdater(fgUpdater);
349                         document.removePositionCategory(CARET_POSITION);
350                         
351                         if (fExitListener != null)
352                                 fExitListener.exit(
353                                         ((flags & COMMIT) != 0) ||
354                                         ((flags & DOCUMENT_CHANGED) != 0));
355
356                 } catch (BadPositionCategoryException e) {
357       PHPeclipsePlugin.log(e);
358                         Assert.isTrue(false);
359                 }
360
361                 if ((flags & DOCUMENT_CHANGED) == 0)
362                         text.redraw();
363         }
364
365         private void next() {
366                 redrawRegion();
367                 
368                 fFramePosition= fManager.getNextPosition(fFramePosition.getOffset());
369                 if (fFramePosition == null) {
370                         leave(UNINSTALL | COMMIT | UPDATE_CARET);
371                 } else {
372                         selectRegion();
373                         redrawRegion();
374                 }
375         }
376         
377         private void previous() {
378                 redrawRegion();
379                 
380                 Position position= fManager.getPreviousPosition(fFramePosition.getOffset());
381                 if (position == null) {
382                         fViewer.getTextWidget().getDisplay().beep();
383                 } else {
384                         fFramePosition= position;
385                         selectRegion();
386                         redrawRegion();
387                 }
388         }
389
390         /*
391          * @see VerifyKeyListener#verifyKey(VerifyEvent)
392          */
393         public void verifyKey(VerifyEvent event) {
394
395                 if (!event.doit)
396                         return;
397                 
398                 Point selection= fViewer.getSelectedRange();
399                 int offset= selection.x;
400                 int length= selection.y;
401                 
402                 ExitFlags exitFlags= fExitPolicy == null ? null : fExitPolicy.doExit(fManager, event, offset, length);
403                 if (exitFlags != null) {
404                         leave(UNINSTALL | exitFlags.flags);
405                         event.doit= exitFlags.doit;
406                         return;
407                 }
408                 
409                 switch (event.character) {
410                 // [SHIFT-]TAB = hop between edit boxes
411                 case 0x09:
412                         {
413                                 // if tab was treated as a document change, would it exceed variable range?
414                                 if (!LinkedPositionManager.includes(fFramePosition, offset, length)) {
415                                         leave(UNINSTALL | COMMIT);
416                                         return;
417                                 }
418                         }
419                 
420                         if (event.stateMask == SWT.SHIFT)
421                                 previous();
422                         else 
423                                 next();                 
424                         
425                         event.doit= false;
426                         break;
427
428                 // ENTER
429                 case 0x0D:
430                         leave(UNINSTALL | COMMIT | UPDATE_CARET);
431                         event.doit= false;
432                         break;
433
434                 // ESC
435                 case 0x1B:
436                         leave(UNINSTALL | COMMIT);
437                         event.doit= false;
438                         break;
439                 }
440         }
441
442         /*
443          * @see VerifyListener#verifyText(VerifyEvent)
444          */
445         public void verifyText(VerifyEvent event) {
446                 if (!event.doit)
447                         return;
448
449
450                 int offset= 0;
451                 int length= 0;
452                 
453                 if (fViewer instanceof ITextViewerExtension3) {
454                         ITextViewerExtension3 extension= (ITextViewerExtension3) fViewer;
455                         IRegion modelRange= extension.widgetRange2ModelRange(new Region(event.start, event.end - event.start));
456                         if (modelRange == null)
457                                 return;
458                                 
459                         offset= modelRange.getOffset();
460                         length= modelRange.getLength();
461                                 
462                 } else {
463                         IRegion visibleRegion= fViewer.getVisibleRegion();
464                         offset= event.start + visibleRegion.getOffset();
465                         length= event.end - event.start;
466                 }
467                 
468                 // allow changes only within linked positions when coming through UI
469                 if (!fManager.anyPositionIncludes(offset, length))
470                         leave(UNINSTALL | COMMIT);
471         }
472
473         /*
474          * @see PaintListener#paintControl(PaintEvent)
475          */
476         public void paintControl(PaintEvent event) {    
477                 if (fFramePosition == null)
478                         return;
479                         
480                 IRegion widgetRange= asWidgetRange(fFramePosition);
481                 if (widgetRange == null) {
482                         leave(UNINSTALL | COMMIT | DOCUMENT_CHANGED);
483                         return;
484                 }
485
486                 int offset= widgetRange.getOffset();
487                 int length= widgetRange.getLength();
488
489                 StyledText text= fViewer.getTextWidget();
490                 
491                 // support for bidi
492                 Point minLocation= getMinimumLocation(text, offset, length);
493                 Point maxLocation= getMaximumLocation(text, offset, length);
494
495                 int x1= minLocation.x;
496                 int x2= minLocation.x + maxLocation.x - minLocation.x - 1;
497                 int y= minLocation.y + text.getLineHeight() - 1;
498                 
499                 GC gc= event.gc;
500                 gc.setForeground(fFrameColor);
501                 gc.drawLine(x1, y, x2, y);
502         }
503         
504         protected IRegion asWidgetRange(Position position) {
505                 if (fViewer instanceof ITextViewerExtension3) {
506                         
507                         ITextViewerExtension3 extension= (ITextViewerExtension3) fViewer;
508                         return extension.modelRange2WidgetRange(new Region(position.getOffset(), position.getLength()));
509                 
510                 } else {
511                         
512                         IRegion region= fViewer.getVisibleRegion();
513                         if (includes(region, position))
514                                 return new Region(position.getOffset() -  region.getOffset(), position.getLength());
515                 }
516                 
517                 return null;
518         }
519
520         private static Point getMinimumLocation(StyledText text, int offset, int length) {
521                 Point minLocation= new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
522
523                 for (int i= 0; i <= length; i++) {
524                         Point location= text.getLocationAtOffset(offset + i);
525                         
526                         if (location.x < minLocation.x)
527                                 minLocation.x= location.x;                      
528                         if (location.y < minLocation.y)
529                                 minLocation.y= location.y;                      
530                 }       
531                 
532                 return minLocation;
533         }
534
535         private static Point getMaximumLocation(StyledText text, int offset, int length) {
536                 Point maxLocation= new Point(Integer.MIN_VALUE, Integer.MIN_VALUE);
537
538                 for (int i= 0; i <= length; i++) {
539                         Point location= text.getLocationAtOffset(offset + i);
540                         
541                         if (location.x > maxLocation.x)
542                                 maxLocation.x= location.x;                      
543                         if (location.y > maxLocation.y)
544                                 maxLocation.y= location.y;                      
545                 }       
546                 
547                 return maxLocation;
548         }
549
550         private void redrawRegion() {
551                 IRegion widgetRange= asWidgetRange(fFramePosition);
552                 if (widgetRange == null) {
553                         leave(UNINSTALL | COMMIT | DOCUMENT_CHANGED);
554                         return;             
555                 }
556                 
557                 StyledText text= fViewer.getTextWidget();
558                 if (text != null && !text.isDisposed()) 
559                         text.redrawRange(widgetRange.getOffset(), widgetRange.getLength(), true);
560         }
561
562         private void selectRegion() {
563                 
564                 IRegion widgetRange= asWidgetRange(fFramePosition);
565                 if (widgetRange == null) {
566                         leave(UNINSTALL | COMMIT | DOCUMENT_CHANGED);
567                         return;   
568                 }
569
570                 StyledText text= fViewer.getTextWidget();
571                 if (text != null && !text.isDisposed()) {
572                         int start= widgetRange.getOffset();
573                         int end= widgetRange.getLength() + start;
574                         text.setSelection(start, end);
575                 }
576         }
577         
578         private void updateCaret() {
579                 
580                 IRegion widgetRange= asWidgetRange(fFramePosition);
581                 if (widgetRange == null) {
582                         leave(UNINSTALL | COMMIT | DOCUMENT_CHANGED);
583                         return;   
584                 }
585                 
586                 int offset= widgetRange.getOffset() + fCaretOffset;
587                 StyledText text= fViewer.getTextWidget();
588                 if (text != null && !text.isDisposed())
589                         text.setCaretOffset(offset);
590         }
591
592         /*
593          * @see ModifyListener#modifyText(ModifyEvent)
594          */      
595         public void modifyText(ModifyEvent e) {
596                 // reposition caret after StyledText
597                 redrawRegion();
598                 updateCaret();
599         }
600
601         private static void handleException(Shell shell, Exception e) {
602                 String title= LinkedPositionMessages.getString("LinkedPositionUI.error.title"); //$NON-NLS-1$
603                 if (e instanceof CoreException)
604                         ExceptionHandler.handle((CoreException)e, shell, title, null);
605                 else if (e instanceof InvocationTargetException)
606                         ExceptionHandler.handle((InvocationTargetException)e, shell, title, null);
607                 else {
608                         MessageDialog.openError(shell, title, e.getMessage());
609       PHPeclipsePlugin.log(e);
610                 }
611         }
612
613         /*
614          * @see ITextInputListener#inputDocumentAboutToBeChanged(IDocument, IDocument)
615          */
616         public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
617                 // 5326: leave linked mode on document change
618                 int flags= UNINSTALL | COMMIT | (oldInput.equals(newInput) ? 0 : DOCUMENT_CHANGED);
619                 leave(flags);
620         }
621
622         /*
623          * @see ITextInputListener#inputDocumentChanged(IDocument, IDocument)
624          */
625         public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
626         }
627
628         private static boolean includes(IRegion region, Position position) {
629                 return
630                         position.getOffset() >= region.getOffset() &&
631                         position.getOffset() + position.getLength() <= region.getOffset() + region.getLength();
632         }
633
634         /*
635          * @see org.eclipse.jface.text.ITextListener#textChanged(TextEvent)
636          */
637         public void textChanged(TextEvent event) {
638                 if (!fNeedRedraw)
639                         return;
640                         
641                 redrawRegion();
642                 fNeedRedraw= false;
643         }
644
645         /*
646          * @see org.eclipse.swt.events.ShellListener#shellActivated(org.eclipse.swt.events.ShellEvent)
647          */
648         public void shellActivated(ShellEvent event) {
649         }
650
651         /*
652          * @see org.eclipse.swt.events.ShellListener#shellClosed(org.eclipse.swt.events.ShellEvent)
653          */
654         public void shellClosed(ShellEvent event) {
655                 leave(UNINSTALL | COMMIT | DOCUMENT_CHANGED);
656         }
657
658         /*
659          * @see org.eclipse.swt.events.ShellListener#shellDeactivated(org.eclipse.swt.events.ShellEvent)
660          */
661         public void shellDeactivated(ShellEvent event) {
662                 leave(UNINSTALL | COMMIT | DOCUMENT_CHANGED);
663         }
664
665         /*
666          * @see org.eclipse.swt.events.ShellListener#shellDeiconified(org.eclipse.swt.events.ShellEvent)
667          */
668         public void shellDeiconified(ShellEvent event) {
669         }
670
671         /*
672          * @see org.eclipse.swt.events.ShellListener#shellIconified(org.eclipse.swt.events.ShellEvent)
673          */
674         public void shellIconified(ShellEvent event) {
675                 leave(UNINSTALL | COMMIT | DOCUMENT_CHANGED);
676         }
677
678 }