| /******************************************************************************* |
| * Copyright (c) 2000, 2005 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.jface.text.contentassist; |
| |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.events.SelectionEvent; |
| import org.eclipse.swt.events.SelectionListener; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Shell; |
| import org.eclipse.swt.widgets.Table; |
| import org.eclipse.swt.widgets.TableItem; |
| |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| |
| import org.eclipse.jface.text.AbstractInformationControlManager; |
| import org.eclipse.jface.text.Assert; |
| import org.eclipse.jface.text.IInformationControl; |
| import org.eclipse.jface.text.IInformationControlCreator; |
| |
| |
| /** |
| * Displays the additional information available for a completion proposal. |
| * |
| * @since 2.0 |
| */ |
| class AdditionalInfoController extends AbstractInformationControlManager { |
| |
| /** |
| * A timer thread. |
| * |
| * @since 3.2 |
| */ |
| private static abstract class Timer { |
| private static final int DELAY_UNTIL_JOB_IS_SCHEDULED= 50; |
| |
| /** |
| * A <code>Task</code> is {@link Task#run() run} when {@link #delay()} milliseconds have |
| * elapsed after it was scheduled without a {@link Timer#reset(ICompletionProposal) reset} |
| * to occur. |
| */ |
| private abstract class Task implements Runnable { |
| /** |
| * @return the delay in milliseconds before this task should be run |
| */ |
| public abstract long delay(); |
| /** |
| * Runs this task. |
| */ |
| public abstract void run(); |
| /** |
| * @return the task to be scheduled after this task has been run |
| */ |
| public abstract Task nextTask(); |
| } |
| |
| /** |
| * IDLE: the initial task, and active whenever the info has been shown. It cannot be run, |
| * but specifies an infinite delay. |
| */ |
| private final Task IDLE= new Task() { |
| public void run() { |
| Assert.isTrue(false); |
| } |
| |
| public Task nextTask() { |
| Assert.isTrue(false); |
| return null; |
| } |
| |
| public long delay() { |
| return Long.MAX_VALUE; |
| } |
| |
| public String toString() { |
| return "IDLE"; //$NON-NLS-1$ |
| } |
| }; |
| /** |
| * FIRST_WAIT: Schedules a platform {@link Job} to fetch additional info from an {@link ICompletionProposalExtension5}. |
| */ |
| private final Task FIRST_WAIT= new Task() { |
| public void run() { |
| final ICompletionProposalExtension5 proposal= getCurrentProposalEx(); |
| Job job= new Job(JFaceTextMessages.getString("AdditionalInfoController.job_name")) { //$NON-NLS-1$ |
| protected IStatus run(IProgressMonitor monitor) { |
| Object info; |
| try { |
| info= proposal.getAdditionalProposalInfo(monitor); |
| } catch (RuntimeException x) { |
| /* |
| * XXX: This is the safest fix at this point so close to end of 3.2. |
| * Will be revisited when fixing https://bugs.eclipse.org/bugs/show_bug.cgi?id=101033 |
| */ |
| return new Status(IStatus.WARNING, "org.eclipse.jface.text", IStatus.OK, "", x); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| setInfo((ICompletionProposal) proposal, info); |
| return new Status(IStatus.OK, "org.eclipse.jface.text", IStatus.OK, "", null); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| }; |
| job.schedule(); |
| } |
| |
| public Task nextTask() { |
| return SECOND_WAIT; |
| } |
| |
| public long delay() { |
| return DELAY_UNTIL_JOB_IS_SCHEDULED; |
| } |
| |
| public String toString() { |
| return "FIRST_WAIT"; //$NON-NLS-1$ |
| } |
| }; |
| /** |
| * SECOND_WAIT: Allows display of additional info obtained from an |
| * {@link ICompletionProposalExtension5}. |
| */ |
| private final Task SECOND_WAIT= new Task() { |
| public void run() { |
| // show the info |
| allowShowing(); |
| } |
| |
| public Task nextTask() { |
| return IDLE; |
| } |
| |
| public long delay() { |
| return fDelay - DELAY_UNTIL_JOB_IS_SCHEDULED; |
| } |
| |
| public String toString() { |
| return "SECOND_WAIT"; //$NON-NLS-1$ |
| } |
| }; |
| /** |
| * LEGACY_WAIT: Posts a runnable into the display thread to fetch additional info from non-{@link ICompletionProposalExtension5}s. |
| */ |
| private final Task LEGACY_WAIT= new Task() { |
| public void run() { |
| final ICompletionProposal proposal= getCurrentProposal(); |
| if (!fDisplay.isDisposed()) { |
| fDisplay.asyncExec(new Runnable() { |
| public void run() { |
| synchronized (Timer.this) { |
| if (proposal == getCurrentProposal()) { |
| Object info= proposal.getAdditionalProposalInfo(); |
| showInformation(proposal, info); |
| } |
| } |
| } |
| }); |
| } |
| } |
| |
| public Task nextTask() { |
| return IDLE; |
| } |
| |
| public long delay() { |
| return fDelay; |
| } |
| |
| public String toString() { |
| return "LEGACY_WAIT"; //$NON-NLS-1$ |
| } |
| }; |
| /** |
| * EXIT: The task that triggers termination of the timer thread. |
| */ |
| private final Task EXIT= new Task() { |
| public long delay() { |
| return 1; |
| } |
| |
| public Task nextTask() { |
| Assert.isTrue(false); |
| return EXIT; |
| } |
| |
| public void run() { |
| Assert.isTrue(false); |
| } |
| |
| public String toString() { |
| return "EXIT"; //$NON-NLS-1$ |
| } |
| }; |
| |
| /** The timer thread. */ |
| private final Thread fThread; |
| |
| /** The currently waiting / active task. */ |
| private Task fTask; |
| /** The next wake up time. */ |
| private long fNextWakeup; |
| |
| private ICompletionProposal fCurrentProposal= null; |
| private Object fCurrentInfo= null; |
| private boolean fAllowShowing= false; |
| |
| private final Display fDisplay; |
| private final int fDelay; |
| |
| /** |
| * Creates a new timer. |
| * |
| * @param display the display to use for display thread posting. |
| * @param delay the delay until to show additional info |
| */ |
| public Timer(Display display, int delay) { |
| fDisplay= display; |
| fDelay= delay; |
| long current= System.currentTimeMillis(); |
| schedule(IDLE, current); |
| |
| fThread= new Thread(new Runnable() { |
| public void run() { |
| try { |
| loop(); |
| } catch (InterruptedException x) { |
| } |
| } |
| }, JFaceTextMessages.getString("InfoPopup.info_delay_timer_name")); //$NON-NLS-1$ |
| fThread.start(); |
| } |
| |
| /** |
| * Terminates the timer thread. |
| */ |
| public synchronized final void terminate() { |
| schedule(EXIT, System.currentTimeMillis()); |
| notifyAll(); |
| } |
| |
| /** |
| * Resets the timer thread as the selection has changed to a new proposal. |
| * |
| * @param p the new proposal |
| */ |
| public final synchronized void reset(ICompletionProposal p) { |
| if (fCurrentProposal != p) { |
| fCurrentProposal= p; |
| fCurrentInfo= null; |
| fAllowShowing= false; |
| |
| long oldWakeup= fNextWakeup; |
| Task task= taskOnReset(p); |
| schedule(task, System.currentTimeMillis()); |
| if (fNextWakeup < oldWakeup) |
| notifyAll(); |
| } |
| } |
| |
| private Task taskOnReset(ICompletionProposal p) { |
| if (p == null) |
| return IDLE; |
| if (isExt5(p)) |
| return FIRST_WAIT; |
| return LEGACY_WAIT; |
| } |
| |
| private synchronized void loop() throws InterruptedException { |
| long current= System.currentTimeMillis(); |
| Task task= currentTask(); |
| |
| while (task != EXIT) { |
| long delay= fNextWakeup - current; |
| if (delay <= 0) { |
| task.run(); |
| task= task.nextTask(); |
| schedule(task, current); |
| } else { |
| wait(delay); |
| current= System.currentTimeMillis(); |
| task= currentTask(); |
| } |
| } |
| } |
| |
| private Task currentTask() { |
| return fTask; |
| } |
| |
| private void schedule(Task task, long current) { |
| fTask= task; |
| long nextWakeup= current + task.delay(); |
| if (nextWakeup <= current) |
| fNextWakeup= Long.MAX_VALUE; |
| else |
| fNextWakeup= nextWakeup; |
| } |
| |
| private boolean isExt5(ICompletionProposal p) { |
| return p instanceof ICompletionProposalExtension5; |
| } |
| |
| ICompletionProposal getCurrentProposal() { |
| return fCurrentProposal; |
| } |
| |
| ICompletionProposalExtension5 getCurrentProposalEx() { |
| Assert.isTrue(fCurrentProposal instanceof ICompletionProposalExtension5); |
| return (ICompletionProposalExtension5) fCurrentProposal; |
| } |
| |
| synchronized void setInfo(ICompletionProposal proposal, Object info) { |
| if (proposal == fCurrentProposal) { |
| fCurrentInfo= info; |
| if (fAllowShowing) { |
| triggerShowing(); |
| } |
| } |
| } |
| |
| private void triggerShowing() { |
| final Object info= fCurrentInfo; |
| if (!fDisplay.isDisposed()) { |
| fDisplay.asyncExec(new Runnable() { |
| public void run() { |
| synchronized (Timer.this) { |
| if (info == fCurrentInfo) { |
| showInformation(fCurrentProposal, info); |
| } |
| } |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Called in the display thread to show additional info. |
| * |
| * @param proposal the proposal to show information about |
| * @param info the information about <code>proposal</code> |
| */ |
| protected abstract void showInformation(ICompletionProposal proposal, Object info); |
| |
| void allowShowing() { |
| fAllowShowing= true; |
| triggerShowing(); |
| } |
| } |
| /** |
| * Internal table selection listener. |
| */ |
| private class TableSelectionListener implements SelectionListener { |
| |
| /* |
| * @see SelectionListener#widgetSelected(SelectionEvent) |
| */ |
| public void widgetSelected(SelectionEvent e) { |
| handleTableSelectionChanged(); |
| } |
| |
| /* |
| * @see SelectionListener#widgetDefaultSelected(SelectionEvent) |
| */ |
| public void widgetDefaultSelected(SelectionEvent e) { |
| } |
| } |
| |
| /** The proposal table. */ |
| private Table fProposalTable; |
| /** The table selection listener */ |
| private SelectionListener fSelectionListener= new TableSelectionListener(); |
| /** The delay after which additional information is displayed */ |
| private final int fDelay; |
| /** |
| * The timer thread. |
| * @since 3.2 |
| */ |
| private Timer fTimer; |
| /** |
| * The proposal most recently set by {@link #showInformation(ICompletionProposal, Object)}, |
| * possibly <code>null</code>. |
| * @since 3.2 |
| */ |
| private ICompletionProposal fProposal; |
| /** |
| * The information most recently set by {@link #showInformation(ICompletionProposal, Object)}, |
| * possibly <code>null</code>. |
| * @since 3.2 |
| */ |
| private Object fInformation; |
| |
| /** |
| * Creates a new additional information controller. |
| * |
| * @param creator the information control creator to be used by this controller |
| * @param delay time in milliseconds after which additional info should be displayed |
| */ |
| AdditionalInfoController(IInformationControlCreator creator, int delay) { |
| super(creator); |
| fDelay= delay; |
| setAnchor(ANCHOR_RIGHT); |
| setFallbackAnchors(new Anchor[] { ANCHOR_RIGHT, ANCHOR_LEFT, ANCHOR_BOTTOM }); |
| setMargins(0, 0); // margin is set depending on the anchor in computeLocation |
| } |
| |
| /* |
| * @see AbstractInformationControlManager#install(Control) |
| */ |
| public void install(Control control) { |
| |
| if (fProposalTable == control) { |
| // already installed |
| return; |
| } |
| |
| super.install(control.getShell()); |
| |
| Assert.isTrue(control instanceof Table); |
| fProposalTable= (Table) control; |
| fProposalTable.addSelectionListener(fSelectionListener); |
| fTimer= new Timer(fProposalTable.getDisplay(), fDelay) { |
| protected void showInformation(ICompletionProposal proposal, Object info) { |
| AdditionalInfoController.this.showInformation(proposal, info); |
| } |
| }; |
| } |
| |
| /* |
| * @see AbstractInformationControlManager#disposeInformationControl() |
| */ |
| public void disposeInformationControl() { |
| |
| if (fTimer !=null) { |
| fTimer.terminate(); |
| fTimer= null; |
| } |
| |
| fProposal= null; |
| fInformation= null; |
| |
| if (fProposalTable != null && !fProposalTable.isDisposed()) { |
| fProposalTable.removeSelectionListener(fSelectionListener); |
| fProposalTable= null; |
| } |
| |
| super.disposeInformationControl(); |
| } |
| |
| /** |
| *Handles a change of the line selected in the associated selector. |
| */ |
| public void handleTableSelectionChanged() { |
| |
| if (fProposalTable != null && !fProposalTable.isDisposed() && fProposalTable.isVisible()) { |
| TableItem[] selection= fProposalTable.getSelection(); |
| if (selection != null && selection.length > 0) { |
| |
| TableItem item= selection[0]; |
| |
| Object d= item.getData(); |
| if (d instanceof ICompletionProposal) { |
| ICompletionProposal p= (ICompletionProposal) d; |
| fTimer.reset(p); |
| } |
| } |
| } |
| } |
| |
| void showInformation(ICompletionProposal proposal, Object info) { |
| if (fProposalTable == null || fProposalTable.isDisposed()) |
| return; |
| |
| if (fProposal == proposal && ((info == null && fInformation == null) || (info != null && info.equals(fInformation)))) |
| return; |
| |
| fInformation= info; |
| fProposal= proposal; |
| showInformation(); |
| } |
| |
| /* |
| * @see AbstractInformationControlManager#computeInformation() |
| */ |
| protected void computeInformation() { |
| if (fProposal instanceof ICompletionProposalExtension3) |
| setCustomInformationControlCreator(((ICompletionProposalExtension3) fProposal).getInformationControlCreator()); |
| else |
| setCustomInformationControlCreator(null); |
| |
| // compute subject area |
| Point size= computeTrueShellSize(fProposalTable.getShell()); |
| |
| // set information & subject area |
| setInformation(fInformation, new Rectangle(0, 0, size.x, size.y)); |
| } |
| |
| /** |
| * Returns the outer size of the given shell, including trim. |
| * |
| * @param shell a shell |
| * @return the shell's outer size |
| * @since 3.2 |
| */ |
| private Point computeTrueShellSize(Shell shell) { |
| Point size= shell.getSize(); |
| if ("gtk".equals(SWT.getPlatform())) { //$NON-NLS-1$ |
| /* XXX bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=136332: on GTK, getSize does not include the trim */ |
| Rectangle trim= shell.computeTrim(0, 0, 0, 0); |
| size.x += trim.width; |
| size.y += trim.height; |
| } |
| return size; |
| } |
| |
| /* |
| * @see org.eclipse.jface.text.AbstractInformationControlManager#computeLocation(org.eclipse.swt.graphics.Rectangle, org.eclipse.swt.graphics.Point, org.eclipse.jface.text.AbstractInformationControlManager.Anchor) |
| */ |
| protected Point computeLocation(Rectangle subjectArea, Point controlSize, Anchor anchor) { |
| Point location= super.computeLocation(subjectArea, controlSize, anchor); |
| |
| /* |
| * Adjust the location by one pixel towards the proposal popup, so that the single pixel |
| * border of the additional info popup overlays with the border of the popup. This avoids |
| * having a double black line. |
| */ |
| int spacing= -1; |
| |
| if (ANCHOR_BOTTOM == anchor) { |
| location.y += spacing; |
| } else if (ANCHOR_RIGHT == anchor) { |
| location.x += spacing; |
| } else if (ANCHOR_TOP == anchor) { |
| location.y -= spacing; |
| } else if (ANCHOR_LEFT == anchor) { |
| location.x -= spacing; |
| } |
| |
| /* |
| * The location is computed using subjectControl.toDisplay(), which does not include the |
| * trim of the subject control. As we want the additional info popup aligned with the outer |
| * coordinates of the proposal popup, adjust this here |
| */ |
| Rectangle trim= fProposalTable.getShell().computeTrim(0, 0, 0, 0); |
| location.x += trim.x; |
| location.y += trim.y; |
| |
| return location; |
| } |
| |
| /* |
| * @see org.eclipse.jface.text.AbstractInformationControlManager#computeSizeConstraints(Control, IInformationControl) |
| */ |
| protected Point computeSizeConstraints(Control subjectControl, IInformationControl informationControl) { |
| Point sizeConstraint= super.computeSizeConstraints(subjectControl, informationControl); |
| Point size= computeTrueShellSize(subjectControl.getShell()); |
| |
| if (sizeConstraint.x < size.x) |
| sizeConstraint.x= size.x; |
| if (sizeConstraint.y < size.y) |
| sizeConstraint.y= size.y; |
| return sizeConstraint; |
| } |
| } |
| |
| |