001: /*******************************************************************************
002: * Copyright (c) 2000, 2006 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.jface.text.contentassist;
011:
012: import org.eclipse.swt.SWT;
013: import org.eclipse.swt.events.SelectionEvent;
014: import org.eclipse.swt.events.SelectionListener;
015: import org.eclipse.swt.graphics.Point;
016: import org.eclipse.swt.graphics.Rectangle;
017: import org.eclipse.swt.widgets.Control;
018: import org.eclipse.swt.widgets.Display;
019: import org.eclipse.swt.widgets.Shell;
020: import org.eclipse.swt.widgets.Table;
021: import org.eclipse.swt.widgets.TableItem;
022:
023: import org.eclipse.core.runtime.Assert;
024: import org.eclipse.core.runtime.IProgressMonitor;
025: import org.eclipse.core.runtime.IStatus;
026: import org.eclipse.core.runtime.Status;
027: import org.eclipse.core.runtime.jobs.Job;
028:
029: import org.eclipse.jface.text.AbstractInformationControlManager;
030: import org.eclipse.jface.text.IInformationControl;
031: import org.eclipse.jface.text.IInformationControlCreator;
032:
033: /**
034: * Displays the additional information available for a completion proposal.
035: *
036: * @since 2.0
037: */
038: class AdditionalInfoController extends
039: AbstractInformationControlManager {
040:
041: /**
042: * A timer thread.
043: *
044: * @since 3.2
045: */
046: private static abstract class Timer {
047: private static final int DELAY_UNTIL_JOB_IS_SCHEDULED = 50;
048:
049: /**
050: * A <code>Task</code> is {@link Task#run() run} when {@link #delay()} milliseconds have
051: * elapsed after it was scheduled without a {@link #reset(ICompletionProposal) reset}
052: * to occur.
053: */
054: private abstract class Task implements Runnable {
055: /**
056: * @return the delay in milliseconds before this task should be run
057: */
058: public abstract long delay();
059:
060: /**
061: * Runs this task.
062: */
063: public abstract void run();
064:
065: /**
066: * @return the task to be scheduled after this task has been run
067: */
068: public abstract Task nextTask();
069: }
070:
071: /**
072: * IDLE: the initial task, and active whenever the info has been shown. It cannot be run,
073: * but specifies an infinite delay.
074: */
075: private final Task IDLE = new Task() {
076: public void run() {
077: Assert.isTrue(false);
078: }
079:
080: public Task nextTask() {
081: Assert.isTrue(false);
082: return null;
083: }
084:
085: public long delay() {
086: return Long.MAX_VALUE;
087: }
088:
089: public String toString() {
090: return "IDLE"; //$NON-NLS-1$
091: }
092: };
093: /**
094: * FIRST_WAIT: Schedules a platform {@link Job} to fetch additional info from an {@link ICompletionProposalExtension5}.
095: */
096: private final Task FIRST_WAIT = new Task() {
097: public void run() {
098: final ICompletionProposalExtension5 proposal = getCurrentProposalEx();
099: Job job = new Job(JFaceTextMessages
100: .getString("AdditionalInfoController.job_name")) { //$NON-NLS-1$
101: protected IStatus run(IProgressMonitor monitor) {
102: Object info;
103: try {
104: info = proposal
105: .getAdditionalProposalInfo(monitor);
106: } catch (RuntimeException x) {
107: /*
108: * XXX: This is the safest fix at this point so close to end of 3.2.
109: * Will be revisited when fixing https://bugs.eclipse.org/bugs/show_bug.cgi?id=101033
110: */
111: return new Status(
112: IStatus.WARNING,
113: "org.eclipse.jface.text", IStatus.OK, "", x); //$NON-NLS-1$ //$NON-NLS-2$
114: }
115: setInfo((ICompletionProposal) proposal, info);
116: return new Status(
117: IStatus.OK,
118: "org.eclipse.jface.text", IStatus.OK, "", null); //$NON-NLS-1$ //$NON-NLS-2$
119: }
120: };
121: job.schedule();
122: }
123:
124: public Task nextTask() {
125: return SECOND_WAIT;
126: }
127:
128: public long delay() {
129: return DELAY_UNTIL_JOB_IS_SCHEDULED;
130: }
131:
132: public String toString() {
133: return "FIRST_WAIT"; //$NON-NLS-1$
134: }
135: };
136: /**
137: * SECOND_WAIT: Allows display of additional info obtained from an
138: * {@link ICompletionProposalExtension5}.
139: */
140: private final Task SECOND_WAIT = new Task() {
141: public void run() {
142: // show the info
143: allowShowing();
144: }
145:
146: public Task nextTask() {
147: return IDLE;
148: }
149:
150: public long delay() {
151: return fDelay - DELAY_UNTIL_JOB_IS_SCHEDULED;
152: }
153:
154: public String toString() {
155: return "SECOND_WAIT"; //$NON-NLS-1$
156: }
157: };
158: /**
159: * LEGACY_WAIT: Posts a runnable into the display thread to fetch additional info from non-{@link ICompletionProposalExtension5}s.
160: */
161: private final Task LEGACY_WAIT = new Task() {
162: public void run() {
163: final ICompletionProposal proposal = getCurrentProposal();
164: if (!fDisplay.isDisposed()) {
165: fDisplay.asyncExec(new Runnable() {
166: public void run() {
167: synchronized (Timer.this ) {
168: if (proposal == getCurrentProposal()) {
169: Object info = proposal
170: .getAdditionalProposalInfo();
171: showInformation(proposal, info);
172: }
173: }
174: }
175: });
176: }
177: }
178:
179: public Task nextTask() {
180: return IDLE;
181: }
182:
183: public long delay() {
184: return fDelay;
185: }
186:
187: public String toString() {
188: return "LEGACY_WAIT"; //$NON-NLS-1$
189: }
190: };
191: /**
192: * EXIT: The task that triggers termination of the timer thread.
193: */
194: private final Task EXIT = new Task() {
195: public long delay() {
196: return 1;
197: }
198:
199: public Task nextTask() {
200: Assert.isTrue(false);
201: return EXIT;
202: }
203:
204: public void run() {
205: Assert.isTrue(false);
206: }
207:
208: public String toString() {
209: return "EXIT"; //$NON-NLS-1$
210: }
211: };
212:
213: /** The timer thread. */
214: private final Thread fThread;
215:
216: /** The currently waiting / active task. */
217: private Task fTask;
218: /** The next wake up time. */
219: private long fNextWakeup;
220:
221: private ICompletionProposal fCurrentProposal = null;
222: private Object fCurrentInfo = null;
223: private boolean fAllowShowing = false;
224:
225: private final Display fDisplay;
226: private final int fDelay;
227:
228: /**
229: * Creates a new timer.
230: *
231: * @param display the display to use for display thread posting.
232: * @param delay the delay until to show additional info
233: */
234: public Timer(Display display, int delay) {
235: fDisplay = display;
236: fDelay = delay;
237: long current = System.currentTimeMillis();
238: schedule(IDLE, current);
239:
240: fThread = new Thread(new Runnable() {
241: public void run() {
242: try {
243: loop();
244: } catch (InterruptedException x) {
245: }
246: }
247: }, JFaceTextMessages
248: .getString("InfoPopup.info_delay_timer_name")); //$NON-NLS-1$
249: fThread.start();
250: }
251:
252: /**
253: * Terminates the timer thread.
254: */
255: public synchronized final void terminate() {
256: schedule(EXIT, System.currentTimeMillis());
257: notifyAll();
258: }
259:
260: /**
261: * Resets the timer thread as the selection has changed to a new proposal.
262: *
263: * @param p the new proposal
264: */
265: public final synchronized void reset(ICompletionProposal p) {
266: if (fCurrentProposal != p) {
267: fCurrentProposal = p;
268: fCurrentInfo = null;
269: fAllowShowing = false;
270:
271: long oldWakeup = fNextWakeup;
272: Task task = taskOnReset(p);
273: schedule(task, System.currentTimeMillis());
274: if (fNextWakeup < oldWakeup)
275: notifyAll();
276: }
277: }
278:
279: private Task taskOnReset(ICompletionProposal p) {
280: if (p == null)
281: return IDLE;
282: if (isExt5(p))
283: return FIRST_WAIT;
284: return LEGACY_WAIT;
285: }
286:
287: private synchronized void loop() throws InterruptedException {
288: long current = System.currentTimeMillis();
289: Task task = currentTask();
290:
291: while (task != EXIT) {
292: long delay = fNextWakeup - current;
293: if (delay <= 0) {
294: task.run();
295: task = task.nextTask();
296: schedule(task, current);
297: } else {
298: wait(delay);
299: current = System.currentTimeMillis();
300: task = currentTask();
301: }
302: }
303: }
304:
305: private Task currentTask() {
306: return fTask;
307: }
308:
309: private void schedule(Task task, long current) {
310: fTask = task;
311: long nextWakeup = current + task.delay();
312: if (nextWakeup <= current)
313: fNextWakeup = Long.MAX_VALUE;
314: else
315: fNextWakeup = nextWakeup;
316: }
317:
318: private boolean isExt5(ICompletionProposal p) {
319: return p instanceof ICompletionProposalExtension5;
320: }
321:
322: ICompletionProposal getCurrentProposal() {
323: return fCurrentProposal;
324: }
325:
326: ICompletionProposalExtension5 getCurrentProposalEx() {
327: Assert
328: .isTrue(fCurrentProposal instanceof ICompletionProposalExtension5);
329: return (ICompletionProposalExtension5) fCurrentProposal;
330: }
331:
332: synchronized void setInfo(ICompletionProposal proposal,
333: Object info) {
334: if (proposal == fCurrentProposal) {
335: fCurrentInfo = info;
336: if (fAllowShowing) {
337: triggerShowing();
338: }
339: }
340: }
341:
342: private void triggerShowing() {
343: final Object info = fCurrentInfo;
344: if (!fDisplay.isDisposed()) {
345: fDisplay.asyncExec(new Runnable() {
346: public void run() {
347: synchronized (Timer.this ) {
348: if (info == fCurrentInfo) {
349: showInformation(fCurrentProposal, info);
350: }
351: }
352: }
353: });
354: }
355: }
356:
357: /**
358: * Called in the display thread to show additional info.
359: *
360: * @param proposal the proposal to show information about
361: * @param info the information about <code>proposal</code>
362: */
363: protected abstract void showInformation(
364: ICompletionProposal proposal, Object info);
365:
366: void allowShowing() {
367: fAllowShowing = true;
368: triggerShowing();
369: }
370: }
371:
372: /**
373: * Internal table selection listener.
374: */
375: private class TableSelectionListener implements SelectionListener {
376:
377: /*
378: * @see SelectionListener#widgetSelected(SelectionEvent)
379: */
380: public void widgetSelected(SelectionEvent e) {
381: handleTableSelectionChanged();
382: }
383:
384: /*
385: * @see SelectionListener#widgetDefaultSelected(SelectionEvent)
386: */
387: public void widgetDefaultSelected(SelectionEvent e) {
388: }
389: }
390:
391: /** The proposal table. */
392: private Table fProposalTable;
393: /** The table selection listener */
394: private SelectionListener fSelectionListener = new TableSelectionListener();
395: /** The delay after which additional information is displayed */
396: private final int fDelay;
397: /**
398: * The timer thread.
399: * @since 3.2
400: */
401: private Timer fTimer;
402: /**
403: * The proposal most recently set by {@link #showInformation(ICompletionProposal, Object)},
404: * possibly <code>null</code>.
405: * @since 3.2
406: */
407: private ICompletionProposal fProposal;
408: /**
409: * The information most recently set by {@link #showInformation(ICompletionProposal, Object)},
410: * possibly <code>null</code>.
411: * @since 3.2
412: */
413: private Object fInformation;
414:
415: /**
416: * Creates a new additional information controller.
417: *
418: * @param creator the information control creator to be used by this controller
419: * @param delay time in milliseconds after which additional info should be displayed
420: */
421: AdditionalInfoController(IInformationControlCreator creator,
422: int delay) {
423: super (creator);
424: fDelay = delay;
425: setAnchor(ANCHOR_RIGHT);
426: setFallbackAnchors(new Anchor[] { ANCHOR_RIGHT, ANCHOR_LEFT,
427: ANCHOR_BOTTOM });
428:
429: /*
430: * Adjust the location by one pixel towards the proposal popup, so that the single pixel
431: * border of the additional info popup overlays with the border of the popup. This avoids
432: * having a double black line.
433: */
434: int spacing = -1;
435: setMargins(spacing, spacing); // see also adjustment in #computeLocation
436: }
437:
438: /*
439: * @see AbstractInformationControlManager#install(Control)
440: */
441: public void install(Control control) {
442:
443: if (fProposalTable == control) {
444: // already installed
445: return;
446: }
447:
448: super .install(control.getShell());
449:
450: Assert.isTrue(control instanceof Table);
451: fProposalTable = (Table) control;
452: fProposalTable.addSelectionListener(fSelectionListener);
453: fTimer = new Timer(fProposalTable.getDisplay(), fDelay) {
454: protected void showInformation(
455: ICompletionProposal proposal, Object info) {
456: AdditionalInfoController.this .showInformation(proposal,
457: info);
458: }
459: };
460: }
461:
462: /*
463: * @see AbstractInformationControlManager#disposeInformationControl()
464: */
465: public void disposeInformationControl() {
466:
467: if (fTimer != null) {
468: fTimer.terminate();
469: fTimer = null;
470: }
471:
472: fProposal = null;
473: fInformation = null;
474:
475: if (fProposalTable != null && !fProposalTable.isDisposed()) {
476: fProposalTable.removeSelectionListener(fSelectionListener);
477: fProposalTable = null;
478: }
479:
480: super .disposeInformationControl();
481: }
482:
483: /**
484: *Handles a change of the line selected in the associated selector.
485: */
486: public void handleTableSelectionChanged() {
487:
488: if (fProposalTable != null && !fProposalTable.isDisposed()
489: && fProposalTable.isVisible()) {
490: TableItem[] selection = fProposalTable.getSelection();
491: if (selection != null && selection.length > 0) {
492:
493: TableItem item = selection[0];
494:
495: Object d = item.getData();
496: if (d instanceof ICompletionProposal) {
497: ICompletionProposal p = (ICompletionProposal) d;
498: fTimer.reset(p);
499: }
500: }
501: }
502: }
503:
504: void showInformation(ICompletionProposal proposal, Object info) {
505: if (fProposalTable == null || fProposalTable.isDisposed())
506: return;
507:
508: if (fProposal == proposal
509: && ((info == null && fInformation == null) || (info != null && info
510: .equals(fInformation))))
511: return;
512:
513: fInformation = info;
514: fProposal = proposal;
515: showInformation();
516: }
517:
518: /*
519: * @see AbstractInformationControlManager#computeInformation()
520: */
521: protected void computeInformation() {
522: if (fProposal instanceof ICompletionProposalExtension3)
523: setCustomInformationControlCreator(((ICompletionProposalExtension3) fProposal)
524: .getInformationControlCreator());
525: else
526: setCustomInformationControlCreator(null);
527:
528: // compute subject area
529: Point size = computeTrueShellSize(fProposalTable.getShell());
530:
531: // set information & subject area
532: setInformation(fInformation,
533: new Rectangle(0, 0, size.x, size.y));
534: }
535:
536: /**
537: * Returns the outer size of the given shell, including trim.
538: *
539: * @param shell a shell
540: * @return the shell's outer size
541: * @since 3.2
542: */
543: private Point computeTrueShellSize(Shell shell) {
544: Point size = shell.getSize();
545: if ("gtk".equals(SWT.getPlatform())) { //$NON-NLS-1$
546: /* XXX bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=136332: on GTK, getSize does not include the trim */
547: Rectangle trim = shell.computeTrim(0, 0, 0, 0);
548: size.x += trim.width;
549: size.y += trim.height;
550: }
551: return size;
552: }
553:
554: /*
555: * @see org.eclipse.jface.text.AbstractInformationControlManager#computeLocation(org.eclipse.swt.graphics.Rectangle, org.eclipse.swt.graphics.Point, org.eclipse.jface.text.AbstractInformationControlManager.Anchor)
556: */
557: protected Point computeLocation(Rectangle subjectArea,
558: Point controlSize, Anchor anchor) {
559: Point location = super .computeLocation(subjectArea,
560: controlSize, anchor);
561:
562: /*
563: * The location is computed using subjectControl.toDisplay(), which does not include the
564: * trim of the subject control. As we want the additional info popup aligned with the outer
565: * coordinates of the proposal popup, adjust this here
566: */
567: Rectangle trim = fProposalTable.getShell().computeTrim(0, 0, 0,
568: 0);
569: location.x += trim.x;
570: location.y += trim.y;
571:
572: return location;
573: }
574:
575: /*
576: * @see org.eclipse.jface.text.AbstractInformationControlManager#computeSizeConstraints(Control, IInformationControl)
577: */
578: protected Point computeSizeConstraints(Control subjectControl,
579: IInformationControl informationControl) {
580: Point sizeConstraint = super .computeSizeConstraints(
581: subjectControl, informationControl);
582: Point size = computeTrueShellSize(subjectControl.getShell());
583:
584: if (sizeConstraint.x < size.x)
585: sizeConstraint.x = size.x;
586: if (sizeConstraint.y < size.y)
587: sizeConstraint.y = size.y;
588: return sizeConstraint;
589: }
590: }
|