# HG changeset patch # User Frank Benoit # Date 1207241780 -7200 # Node ID f12d40e7da8ffba9327d17c23a77b0ff1f51a6af # Parent 50b0163e18f888ba64a9fa129b088d71648c08e0 fieldassist diff -r 50b0163e18f8 -r f12d40e7da8f dwtx/jface/fieldassist/AutoCompleteField.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/AutoCompleteField.d Thu Apr 03 18:56:20 2008 +0200 @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2006, 2007 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 + * Port to the D programming language: + * Frank Benoit + *******************************************************************************/ +module dwtx.jface.fieldassist.AutoCompleteField; + +import dwtx.jface.fieldassist.IControlContentAdapter; +import dwtx.jface.fieldassist.SimpleContentProposalProvider; +import dwtx.jface.fieldassist.ContentProposalAdapter; + +import dwt.widgets.Control; + +import dwt.dwthelper.utils; + +/** + * AutoCompleteField is a class which attempts to auto-complete a user's + * keystrokes by activating a popup that filters a list of proposals according + * to the content typed by the user. + * + * @see ContentProposalAdapter + * @see SimpleContentProposalProvider + * + * @since 3.3 + */ +public class AutoCompleteField { + + private SimpleContentProposalProvider proposalProvider; + private ContentProposalAdapter adapter; + + /** + * Construct an AutoComplete field on the specified control, whose + * completions are characterized by the specified array of Strings. + * + * @param control + * the control for which autocomplete is desired. May not be + * null. + * @param controlContentAdapter + * the IControlContentAdapter used to obtain and + * update the control's contents. May not be null. + * @param proposals + * the array of Strings representing valid content proposals for + * the field. + */ + public this(Control control, + IControlContentAdapter controlContentAdapter, String[] proposals) { + proposalProvider = new SimpleContentProposalProvider(proposals); + proposalProvider.setFiltering(true); + adapter = new ContentProposalAdapter(control, controlContentAdapter, + proposalProvider, null, null); + adapter.setPropagateKeys(true); + adapter + .setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE); + } + + /** + * Set the Strings to be used as content proposals. + * + * @param proposals + * the array of Strings to be used as proposals. + */ + public void setProposals(String[] proposals) { + proposalProvider.setProposals(proposals); + } +} diff -r 50b0163e18f8 -r f12d40e7da8f dwtx/jface/fieldassist/ComboContentAdapter.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/ComboContentAdapter.d Thu Apr 03 18:56:20 2008 +0200 @@ -0,0 +1,117 @@ +/******************************************************************************* + * Copyright (c) 2005, 2006 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 + * Port to the D programming language: + * Frank Benoit + *******************************************************************************/ +module dwtx.jface.fieldassist.ComboContentAdapter; + +import dwtx.jface.fieldassist.IControlContentAdapter; + +import dwt.graphics.GC; +import dwt.graphics.Point; +import dwt.graphics.Rectangle; +import dwt.widgets.Combo; +import dwt.widgets.Control; + +import dwt.dwthelper.utils; +import tango.text.Text; +alias tango.text.Text.Text!(char) StringBuffer; +/** + * An {@link IControlContentAdapter} for DWT Combo controls. This is a + * convenience class for easily creating a {@link ContentProposalAdapter} for + * combo fields. + * + * @since 3.2 + */ +public class ComboContentAdapter : IControlContentAdapter { + + /* + * (non-Javadoc) + * + * @see dwtx.jface.dialogs.taskassistance.IControlContentAdapter#getControlContents(dwt.widgets.Control) + */ + public String getControlContents(Control control) { + return (cast(Combo) control).getText(); + } + + /* + * (non-Javadoc) + * + * @see dwtx.jface.fieldassist.IControlContentAdapter#setControlContents(dwt.widgets.Control, + * java.lang.String, int) + */ + public void setControlContents(Control control, String text, + int cursorPosition) { + (cast(Combo) control).setText(text); + (cast(Combo) control) + .setSelection(new Point(cursorPosition, cursorPosition)); + } + + /* + * (non-Javadoc) + * + * @see dwtx.jface.fieldassist.IControlContentAdapter#insertControlContents(dwt.widgets.Control, + * java.lang.String, int) + */ + public void insertControlContents(Control control, String text, + int cursorPosition) { + Combo combo = cast(Combo) control; + String contents = combo.getText(); + Point selection = combo.getSelection(); + StringBuffer sb = new StringBuffer(); + sb.append(contents.substring(0, selection.x)); + sb.append(text); + if (selection.y < contents.length) { + sb.append(contents.substring(selection.y, contents.length)); + } + combo.setText(sb.toString()); + selection.x = selection.x + cursorPosition; + selection.y = selection.x; + combo.setSelection(selection); + } + + /* + * (non-Javadoc) + * + * @see dwtx.jface.fieldassist.IControlContentAdapter#getCursorPosition(dwt.widgets.Control) + */ + public int getCursorPosition(Control control) { + return (cast(Combo) control).getSelection().x; + } + + /* + * (non-Javadoc) + * + * @see dwtx.jface.fieldassist.IControlContentAdapter#getInsertionBounds(dwt.widgets.Control) + */ + public Rectangle getInsertionBounds(Control control) { + Combo combo = cast(Combo) control; + int position = combo.getSelection().y; + String contents = combo.getText(); + GC gc = new GC(combo); + gc.setFont(combo.getFont()); + Point extent = gc.textExtent(contents.substring(0, Math.min(position, + contents.length))); + gc.dispose(); + return new Rectangle(combo.getClientArea().x + extent.x, combo + .getClientArea().y, 1, combo.getClientArea().height); + } + + /* + * (non-Javadoc) + * + * @see dwtx.jface.fieldassist.IControlContentAdapter#setCursorPosition(dwt.widgets.Control, + * int) + */ + public void setCursorPosition(Control control, int index) { + (cast(Combo) control).setSelection(new Point(index, index)); + } + +} diff -r 50b0163e18f8 -r f12d40e7da8f dwtx/jface/fieldassist/ContentProposalAdapter.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/ContentProposalAdapter.d Thu Apr 03 18:56:20 2008 +0200 @@ -0,0 +1,2051 @@ +/******************************************************************************* + * Copyright (c) 2005, 2007 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 + * Port to the D programming language: + * Frank Benoit + *******************************************************************************/ +module dwtx.jface.fieldassist.ContentProposalAdapter; + +import dwtx.jface.fieldassist.IContentProposal; +import dwtx.jface.fieldassist.IContentProposalProvider; +import dwtx.jface.fieldassist.IControlContentAdapter; +import dwtx.jface.fieldassist.IContentProposalListener; +import dwtx.jface.fieldassist.IContentProposalListener2; + +import tango.util.collection.ArraySeq; + +import dwt.DWT; +import dwt.events.DisposeEvent; +import dwt.events.DisposeListener; +import dwt.events.FocusAdapter; +import dwt.events.FocusEvent; +import dwt.events.SelectionEvent; +import dwt.events.SelectionListener; +import dwt.graphics.Image; +import dwt.graphics.Point; +import dwt.graphics.Rectangle; +import dwt.layout.GridData; +import dwt.widgets.Composite; +import dwt.widgets.Control; +import dwt.widgets.Event; +import dwt.widgets.Listener; +import dwt.widgets.ScrollBar; +import dwt.widgets.Shell; +import dwt.widgets.Table; +import dwt.widgets.TableItem; +import dwt.widgets.Text; +import dwtx.core.runtime.Assert; +import dwtx.core.runtime.ListenerList; +import dwtx.jface.bindings.keys.KeyStroke; +import dwtx.jface.dialogs.PopupDialog; +import dwtx.jface.viewers.ILabelProvider; + +import dwt.dwthelper.utils; +import dwt.dwthelper.Runnable; +import tango.core.Thread; +static import tango.text.Text; +import tango.io.Stdout; +import tango.text.convert.Format; +alias tango.text.Text.Text!(char) StringBuffer; +/** + * ContentProposalAdapter can be used to attach content proposal behavior to a + * control. This behavior includes obtaining proposals, opening a popup dialog, + * managing the content of the control relative to the selections in the popup, + * and optionally opening up a secondary popup to further describe proposals. + *

+ * A number of configurable options are provided to determine how the control + * content is altered when a proposal is chosen, how the content proposal popup + * is activated, and whether any filtering should be done on the proposals as + * the user types characters. + *

+ * This class is not intended to be subclassed. + * + * @since 3.2 + */ +public class ContentProposalAdapter { + + /* + * The lightweight popup used to show content proposals for a text field. If + * additional information exists for a proposal, then selecting that + * proposal will result in the information being displayed in a secondary + * popup. + */ + class ContentProposalPopup : PopupDialog { + /* + * The listener we install on the popup and related controls to + * determine when to close the popup. Some events (move, resize, close, + * deactivate) trigger closure as soon as they are received, simply + * because one of the registered listeners received them. Other events + * depend on additional circumstances. + */ + private final class PopupCloserListener : Listener { + private bool scrollbarClicked = false; + + public void handleEvent(Event e) { + + // If focus is leaving an important widget or the field's + // shell is deactivating + if (e.type is DWT.FocusOut) { + scrollbarClicked = false; + /* + * Ignore this event if it's only happening because focus is + * moving between the popup shells, their controls, or a + * scrollbar. Do this in an async since the focus is not + * actually switched when this event is received. + */ + e.display.asyncExec(new class Runnable { + Event e_; + this(){ e_=e; } + public void run() { + if (isValid()) { + if (scrollbarClicked + || hasFocus() + || (infoPopup !is null && infoPopup + .hasFocus())) { + return; + } + // Workaround a problem on X and Mac, whereby at + // this point, the focus control is not known. + // This can happen, for example, when resizing + // the popup shell on the Mac. + // Check the active shell. + Shell activeShell = e_.display.getActiveShell(); + if (activeShell is getShell() + || (infoPopup !is null && infoPopup + .getShell() is activeShell)) { + return; + } + /* + * System.out.println(e); + * System.out.println(e.display.getFocusControl()); + * System.out.println(e.display.getActiveShell()); + */ + close(); + } + } + }); + return; + } + + // Scroll bar has been clicked. Remember this for focus event + // processing. + if (e.type is DWT.Selection) { + scrollbarClicked = true; + return; + } + // For all other events, merely getting them dictates closure. + close(); + } + + // Install the listeners for events that need to be monitored for + // popup closure. + void installListeners() { + // Listeners on this popup's table and scroll bar + proposalTable.addListener(DWT.FocusOut, this); + ScrollBar scrollbar = proposalTable.getVerticalBar(); + if (scrollbar !is null) { + scrollbar.addListener(DWT.Selection, this); + } + + // Listeners on this popup's shell + getShell().addListener(DWT.Deactivate, this); + getShell().addListener(DWT.Close, this); + + // Listeners on the target control + control.addListener(DWT.MouseDoubleClick, this); + control.addListener(DWT.MouseDown, this); + control.addListener(DWT.Dispose, this); + control.addListener(DWT.FocusOut, this); + // Listeners on the target control's shell + Shell controlShell = control.getShell(); + controlShell.addListener(DWT.Move, this); + controlShell.addListener(DWT.Resize, this); + + } + + // Remove installed listeners + void removeListeners() { + if (isValid()) { + proposalTable.removeListener(DWT.FocusOut, this); + ScrollBar scrollbar = proposalTable.getVerticalBar(); + if (scrollbar !is null) { + scrollbar.removeListener(DWT.Selection, this); + } + + getShell().removeListener(DWT.Deactivate, this); + getShell().removeListener(DWT.Close, this); + } + + if (control !is null && !control.isDisposed()) { + + control.removeListener(DWT.MouseDoubleClick, this); + control.removeListener(DWT.MouseDown, this); + control.removeListener(DWT.Dispose, this); + control.removeListener(DWT.FocusOut, this); + + Shell controlShell = control.getShell(); + controlShell.removeListener(DWT.Move, this); + controlShell.removeListener(DWT.Resize, this); + } + } + } + + /* + * The listener we will install on the target control. + */ + private final class TargetControlListener : Listener { + // Key events from the control + public void handleEvent(Event e) { + if (!isValid()) { + return; + } + + char key = e.character; + + // Traverse events are handled depending on whether the + // event has a character. + if (e.type is DWT.Traverse) { + // If the traverse event contains a legitimate character, + // then we must set doit false so that the widget will + // receive the key event. We return immediately so that + // the character is handled only in the key event. + // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=132101 + if (key !is 0) { + e.doit = false; + return; + } + // Traversal does not contain a character. Set doit true + // to indicate TRAVERSE_NONE will occur and that no key + // event will be triggered. We will check for navigation + // keys below. + e.detail = DWT.TRAVERSE_NONE; + e.doit = true; + } else { + // Default is to only propagate when configured that way. + // Some keys will always set doit to false anyway. + e.doit = propagateKeys; + } + + // No character. Check for navigation keys. + + if (key is 0) { + int newSelection = proposalTable.getSelectionIndex(); + int visibleRows = (proposalTable.getSize().y / proposalTable + .getItemHeight()) - 1; + switch (e.keyCode) { + case DWT.ARROW_UP: + newSelection -= 1; + if (newSelection < 0) { + newSelection = proposalTable.getItemCount() - 1; + } + // Not typical - usually we get this as a Traverse and + // therefore it never propagates. Added for consistency. + if (e.type is DWT.KeyDown) { + // don't propagate to control + e.doit = false; + } + + break; + + case DWT.ARROW_DOWN: + newSelection += 1; + if (newSelection > proposalTable.getItemCount() - 1) { + newSelection = 0; + } + // Not typical - usually we get this as a Traverse and + // therefore it never propagates. Added for consistency. + if (e.type is DWT.KeyDown) { + // don't propagate to control + e.doit = false; + } + + break; + + case DWT.PAGE_DOWN: + newSelection += visibleRows; + if (newSelection >= proposalTable.getItemCount()) { + newSelection = proposalTable.getItemCount() - 1; + } + if (e.type is DWT.KeyDown) { + // don't propagate to control + e.doit = false; + } + break; + + case DWT.PAGE_UP: + newSelection -= visibleRows; + if (newSelection < 0) { + newSelection = 0; + } + if (e.type is DWT.KeyDown) { + // don't propagate to control + e.doit = false; + } + break; + + case DWT.HOME: + newSelection = 0; + if (e.type is DWT.KeyDown) { + // don't propagate to control + e.doit = false; + } + break; + + case DWT.END: + newSelection = proposalTable.getItemCount() - 1; + if (e.type is DWT.KeyDown) { + // don't propagate to control + e.doit = false; + } + break; + + // If received as a Traverse, these should propagate + // to the control as keydown. If received as a keydown, + // proposals should be recomputed since the cursor + // position has changed. + case DWT.ARROW_LEFT: + case DWT.ARROW_RIGHT: + if (e.type is DWT.Traverse) { + e.doit = false; + } else { + e.doit = true; + String contents = getControlContentAdapter() + .getControlContents(getControl()); + // If there are no contents, changes in cursor + // position + // have no effect. Note also that we do not affect + // the filter + // text on ARROW_LEFT as we would with BS. + if (contents.length > 0) { + asyncRecomputeProposals(filterText); + } + } + break; + + // Any unknown keycodes will cause the popup to close. + // Modifier keys are explicitly checked and ignored because + // they are not complete yet (no character). + default: + if (e.keyCode !is DWT.CAPS_LOCK && e.keyCode !is DWT.MOD1 + && e.keyCode !is DWT.MOD2 + && e.keyCode !is DWT.MOD3 + && e.keyCode !is DWT.MOD4) { + close(); + } + return; + } + + // If any of these navigation events caused a new selection, + // then handle that now and return. + if (newSelection >= 0) { + selectProposal(newSelection); + } + return; + } + + // key !is 0 + // Check for special keys involved in cancelling, accepting, or + // filtering the proposals. + switch (key) { + case DWT.ESC: + e.doit = false; + close(); + break; + + case DWT.LF: + case DWT.CR: + e.doit = false; + Object p = cast(Object)getSelectedProposal(); + if (p !is null) { + acceptCurrentProposal(); + } else { + close(); + } + break; + + case DWT.TAB: + e.doit = false; + getShell().setFocus(); + return; + + case DWT.BS: + // Backspace should back out of any stored filter text + if (filterStyle !is FILTER_NONE) { + // We have no filter to back out of, so do nothing + if (filterText.length is 0) { + return; + } + // There is filter to back out of + filterText = filterText.substring(0, filterText + .length - 1); + asyncRecomputeProposals(filterText); + return; + } + // There is no filtering provided by us, but some + // clients provide their own filtering based on content. + // Recompute the proposals if the cursor position + // will change (is not at 0). + int pos = getControlContentAdapter().getCursorPosition( + getControl()); + // We rely on the fact that the contents and pos do not yet + // reflect the result of the BS. If the contents were + // already empty, then BS should not cause + // a recompute. + if (pos > 0) { + asyncRecomputeProposals(filterText); + } + break; + + default: + // If the key is a defined unicode character, and not one of + // the special cases processed above, update the filter text + // and filter the proposals. + if (CharacterIsDefined(key)) { + if (filterStyle is FILTER_CUMULATIVE) { + filterText = filterText ~ dcharToString(key); + } else if (filterStyle is FILTER_CHARACTER) { + filterText = dcharToString(key); + } + // Recompute proposals after processing this event. + asyncRecomputeProposals(filterText); + } + break; + } + } + } + + /* + * Internal class used to implement the secondary popup. + */ + private class InfoPopupDialog : PopupDialog { + + /* + * The text control that displays the text. + */ + private Text text; + + /* + * The String shown in the popup. + */ + private String contents = EMPTY; + + /* + * Construct an info-popup with the specified parent. + */ + this(Shell parent) { + super(parent, PopupDialog.HOVER_SHELLSTYLE, false, false, + false, false, null, null); + } + + /* + * Create a text control for showing the info about a proposal. + */ + protected Control createDialogArea(Composite parent) { + text = new Text(parent, DWT.MULTI | DWT.READ_ONLY | DWT.WRAP + | DWT.NO_FOCUS); + + // Use the compact margins employed by PopupDialog. + GridData gd = new GridData(GridData.BEGINNING + | GridData.FILL_BOTH); + gd.horizontalIndent = PopupDialog.POPUP_HORIZONTALSPACING; + gd.verticalIndent = PopupDialog.POPUP_VERTICALSPACING; + text.setLayoutData(gd); + text.setText(contents); + + // since DWT.NO_FOCUS is only a hint... + text.addFocusListener(new class FocusAdapter { + public void focusGained(FocusEvent event) { + this.outer.close(); + } + }); + return text; + } + + /* + * Adjust the bounds so that we appear adjacent to our parent shell + */ + protected void adjustBounds() { + Rectangle parentBounds = getParentShell().getBounds(); + Rectangle proposedBounds; + // Try placing the info popup to the right + Rectangle rightProposedBounds = new Rectangle(parentBounds.x + + parentBounds.width + + PopupDialog.POPUP_HORIZONTALSPACING, parentBounds.y + + PopupDialog.POPUP_VERTICALSPACING, + parentBounds.width, parentBounds.height); + rightProposedBounds = getConstrainedShellBounds(rightProposedBounds); + // If it won't fit on the right, try the left + if (rightProposedBounds.intersects(parentBounds)) { + Rectangle leftProposedBounds = new Rectangle(parentBounds.x + - parentBounds.width - POPUP_HORIZONTALSPACING - 1, + parentBounds.y, parentBounds.width, + parentBounds.height); + leftProposedBounds = getConstrainedShellBounds(leftProposedBounds); + // If it won't fit on the left, choose the proposed bounds + // that fits the best + if (leftProposedBounds.intersects(parentBounds)) { + if (rightProposedBounds.x - parentBounds.x >= parentBounds.x + - leftProposedBounds.x) { + rightProposedBounds.x = parentBounds.x + + parentBounds.width + + PopupDialog.POPUP_HORIZONTALSPACING; + proposedBounds = rightProposedBounds; + } else { + leftProposedBounds.width = parentBounds.x + - POPUP_HORIZONTALSPACING + - leftProposedBounds.x; + proposedBounds = leftProposedBounds; + } + } else { + // use the proposed bounds on the left + proposedBounds = leftProposedBounds; + } + } else { + // use the proposed bounds on the right + proposedBounds = rightProposedBounds; + } + getShell().setBounds(proposedBounds); + } + + /* + * Set the text contents of the popup. + */ + void setContents(String newContents) { + if (newContents is null) { + newContents = EMPTY; + } + this.contents = newContents; + if (text !is null && !text.isDisposed()) { + text.setText(contents); + } + } + + /* + * Return whether the popup has focus. + */ + bool hasFocus() { + if (text is null || text.isDisposed()) { + return false; + } + return text.getShell().isFocusControl() + || text.isFocusControl(); + } + } + + /* + * The listener installed on the target control. + */ + private Listener targetControlListener; + + /* + * The listener installed in order to close the popup. + */ + private PopupCloserListener popupCloser; + + /* + * The table used to show the list of proposals. + */ + private Table proposalTable; + + /* + * The proposals to be shown (cached to avoid repeated requests). + */ + private IContentProposal[] proposals; + + /* + * Secondary popup used to show detailed information about the selected + * proposal.. + */ + private InfoPopupDialog infoPopup; + + /* + * Flag indicating whether there is a pending secondary popup update. + */ + private bool pendingDescriptionUpdate = false; + + /* + * Filter text - tracked while popup is open, only if we are told to + * filter + */ + private String filterText = EMPTY; + + /** + * Constructs a new instance of this popup, specifying the control for + * which this popup is showing content, and how the proposals should be + * obtained and displayed. + * + * @param infoText + * Text to be shown in a lower info area, or + * null if there is no info area. + */ + this(String infoText, IContentProposal[] proposals) { + // IMPORTANT: Use of DWT.ON_TOP is critical here for ensuring + // that the target control retains focus on Mac and Linux. Without + // it, the focus will disappear, keystrokes will not go to the + // popup, and the popup closer will wrongly close the popup. + // On platforms where DWT.ON_TOP overrides DWT.RESIZE, we will live + // with this. + // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=126138 + super(control.getShell(), DWT.RESIZE | DWT.ON_TOP, false, false, + false, false, null, infoText); + this.proposals = proposals; + } + + /* + * Overridden to force change of colors. See + * https://bugs.eclipse.org/bugs/show_bug.cgi?id=136244 (non-Javadoc) + * + * @see dwtx.jface.dialogs.PopupDialog#createContents(dwt.widgets.Composite) + */ + protected Control createContents(Composite parent) { + Control contents = super.createContents(parent); + changeDefaultColors(parent); + return contents; + } + + /* + * Set the colors of the popup. The contents have already been created. + */ + private void changeDefaultColors(Control control) { + applyForegroundColor(getShell().getDisplay().getSystemColor( + DWT.COLOR_LIST_FOREGROUND), control); + applyBackgroundColor(getShell().getDisplay().getSystemColor( + DWT.COLOR_LIST_BACKGROUND), control); + } + + /* + * Creates the content area for the proposal popup. This creates a table + * and places it inside the composite. The table will contain a list of + * all the proposals. + * + * @param parent The parent composite to contain the dialog area; must + * not be null. + */ + protected final Control createDialogArea(Composite parent) { + // Use virtual where appropriate (see flag definition). + if (USE_VIRTUAL) { + proposalTable = new Table(parent, DWT.H_SCROLL | DWT.V_SCROLL + | DWT.VIRTUAL); + + Listener listener = new class Listener { + public void handleEvent(Event event) { + handleSetData(event); + } + }; + proposalTable.addListener(DWT.SetData, listener); + } else { + proposalTable = new Table(parent, DWT.H_SCROLL | DWT.V_SCROLL); + } + + // set the proposals to force population of the table. + setProposals(filterProposals(proposals, filterText)); + + proposalTable.setHeaderVisible(false); + proposalTable.addSelectionListener(new class SelectionListener { + + public void widgetSelected(SelectionEvent e) { + // If a proposal has been selected, show it in the secondary + // popup. Otherwise close the popup. + if (e.item is null) { + if (infoPopup !is null) { + infoPopup.close(); + } + } else { + showProposalDescription(); + } + } + + // Default selection was made. Accept the current proposal. + public void widgetDefaultSelected(SelectionEvent e) { + acceptCurrentProposal(); + } + }); + return proposalTable; + } + + /* + * (non-Javadoc) + * + * @see dwtx.jface.dialogs.PopupDialog.adjustBounds() + */ + protected void adjustBounds() { + // Get our control's location in display coordinates. + Point location = control.getDisplay().map(control.getParent(), + null, control.getLocation()); + int initialX = location.x + POPUP_OFFSET; + int initialY = location.y + control.getSize().y + POPUP_OFFSET; + // If we are inserting content, use the cursor position to + // position the control. + if (getProposalAcceptanceStyle() is PROPOSAL_INSERT) { + Rectangle insertionBounds = controlContentAdapter + .getInsertionBounds(control); + initialX = initialX + insertionBounds.x; + initialY = location.y + insertionBounds.y + + insertionBounds.height; + } + + // If there is no specified size, force it by setting + // up a layout on the table. + if (popupSize is null) { + GridData data = new GridData(GridData.FILL_BOTH); + data.heightHint = proposalTable.getItemHeight() + * POPUP_CHAR_HEIGHT; + data.widthHint = Math.max(control.getSize().x, + POPUP_MINIMUM_WIDTH); + proposalTable.setLayoutData(data); + getShell().pack(); + popupSize = getShell().getSize(); + } + getShell().setBounds(initialX, initialY, popupSize.x, popupSize.y); + + // Now set up a listener to monitor any changes in size. + getShell().addListener(DWT.Resize, new class Listener { + public void handleEvent(Event e) { + popupSize = getShell().getSize(); + if (infoPopup !is null) { + infoPopup.adjustBounds(); + } + } + }); + } + + /* + * Handle the set data event. Set the item data of the requested item to + * the corresponding proposal in the proposal cache. + */ + private void handleSetData(Event event) { + TableItem item = cast(TableItem) event.item; + int index = proposalTable.indexOf(item); + + if (0 <= index && index < proposals.length) { + IContentProposal current = proposals[index]; + item.setText(getString(current)); + item.setImage(getImage(current)); + item.setData(cast(Object)current); + } else { + // this should not happen, but does on win32 + } + } + + /* + * Caches the specified proposals and repopulates the table if it has + * been created. + */ + private void setProposals(IContentProposal[] newProposals) { + if (newProposals is null || newProposals.length is 0) { + newProposals = getEmptyProposalArray(); + } + this.proposals = newProposals; + + // If there is a table + if (isValid()) { + final int newSize = newProposals.length; + if (USE_VIRTUAL) { + // Set and clear the virtual table. Data will be + // provided in the DWT.SetData event handler. + proposalTable.setItemCount(newSize); + proposalTable.clearAll(); + } else { + // Populate the table manually + proposalTable.setRedraw(false); + proposalTable.setItemCount(newSize); + TableItem[] items = proposalTable.getItems(); + for (int i = 0; i < items.length; i++) { + TableItem item = items[i]; + IContentProposal proposal = newProposals[i]; + item.setText(getString(proposal)); + item.setImage(getImage(proposal)); + item.setData(cast(Object)proposal); + } + proposalTable.setRedraw(true); + } + // Default to the first selection if there is content. + if (newProposals.length > 0) { + selectProposal(0); + } else { + // No selection, close the secondary popup if it was open + if (infoPopup !is null) { + infoPopup.close(); + } + + } + } + } + + /* + * Get the string for the specified proposal. Always return a String of + * some kind. + */ + private String getString(IContentProposal proposal) { + if (proposal is null) { + return EMPTY; + } + if (labelProvider is null) { + return proposal.getLabel() is null ? proposal.getContent() + : proposal.getLabel(); + } + return labelProvider.getText(cast(Object)proposal); + } + + /* + * Get the image for the specified proposal. If there is no image + * available, return null. + */ + private Image getImage(IContentProposal proposal) { + if (proposal is null || labelProvider is null) { + return null; + } + return labelProvider.getImage(cast(Object)proposal); + } + + /* + * Return an empty array. Used so that something always shows in the + * proposal popup, even if no proposal provider was specified. + */ + private IContentProposal[] getEmptyProposalArray() { + return new IContentProposal[0]; + } + + /* + * Answer true if the popup is valid, which means the table has been + * created and not disposed. + */ + private bool isValid() { + return proposalTable !is null && !proposalTable.isDisposed(); + } + + /* + * Return whether the receiver has focus. + */ + private bool hasFocus() { + if (!isValid()) { + return false; + } + return getShell().isFocusControl() + || proposalTable.isFocusControl(); + } + + /* + * Return the current selected proposal. + */ + private IContentProposal getSelectedProposal() { + if (isValid()) { + int i = proposalTable.getSelectionIndex(); + if (proposals is null || i < 0 || i >= proposals.length) { + return null; + } + return proposals[i]; + } + return null; + } + + /* + * Select the proposal at the given index. + */ + private void selectProposal(int index) { + Assert + .isTrue(index >= 0, + "Proposal index should never be negative"); //$NON-NLS-1$ + if (!isValid() || proposals is null || index >= proposals.length) { + return; + } + proposalTable.setSelection(index); + proposalTable.showSelection(); + + showProposalDescription(); + } + + /** + * Opens this ContentProposalPopup. This method is extended in order to + * add the control listener when the popup is opened and to invoke the + * secondary popup if applicable. + * + * @return the return code + * + * @see dwtx.jface.window.Window#open() + */ + public int open() { + int value = super.open(); + if (popupCloser is null) { + popupCloser = new PopupCloserListener(); + } + popupCloser.installListeners(); + IContentProposal p = getSelectedProposal(); + if (p !is null) { + showProposalDescription(); + } + return value; + } + + /** + * Closes this popup. This method is extended to remove the control + * listener. + * + * @return true if the window is (or was already) closed, + * and false if it is still open + */ + public bool close() { + popupCloser.removeListeners(); + if (infoPopup !is null) { + infoPopup.close(); + } + bool ret = super.close(); + notifyPopupClosed(); + return ret; + } + + /* + * Show the currently selected proposal's description in a secondary + * popup. + */ + private void showProposalDescription() { + // If we do not already have a pending update, then + // create a thread now that will show the proposal description + if (!pendingDescriptionUpdate) { + // Create a thread that will sleep for the specified delay + // before creating the popup. We do not use Jobs since this + // code must be able to run independently of the Eclipse + // runtime. + auto r = new class() Runnable { + public void run() { + pendingDescriptionUpdate = true; + + try { + Thread.sleep( POPUP_DELAY / 1000.0 ); + } + catch (InterruptedException e) { + } + + if (!isValid()) { + return; + } + getShell().getDisplay().syncExec(new class() Runnable { + public void run() { + // Query the current selection since we have + // been delayed + IContentProposal p = getSelectedProposal(); + if (p !is null) { + String description = p.getDescription(); + if (description !is null) { + if (infoPopup is null) { + infoPopup = new InfoPopupDialog( + getShell()); + infoPopup.open(); + infoPopup + .getShell() + .addDisposeListener( + new class DisposeListener { + public void widgetDisposed( + DisposeEvent event) { + infoPopup = null; + } + }); + } + infoPopup.setContents(p + .getDescription()); + } else if (infoPopup !is null) { + infoPopup.close(); + } + pendingDescriptionUpdate = false; + + } + } + }); + } + }; + Thread t = new Thread(&r.run); + t.start(); + } + } + + /* + * Accept the current proposal. + */ + private void acceptCurrentProposal() { + // Close before accepting the proposal. + // This is important so that the cursor position can be + // properly restored at acceptance, which does not work without + // focus on some controls. + // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=127108 + IContentProposal proposal = getSelectedProposal(); + close(); + proposalAccepted(proposal); + } + + /* + * Request the proposals from the proposal provider, and recompute any + * caches. Repopulate the popup if it is open. + */ + private void recomputeProposals(String filterText) { + IContentProposal[] allProposals = getProposals(); + // If the non-filtered proposal list is empty, we should + // close the popup. + // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=147377 + if (allProposals.length is 0) { + proposals = allProposals; + close(); + } else { + // Keep the popup open, but filter by any provided filter text + setProposals(filterProposals(allProposals, filterText)); + } + } + + /* + * In an async block, request the proposals. This is used when clients + * are in the middle of processing an event that affects the widget + * content. By using an async, we ensure that the widget content is up + * to date with the event. + */ + private void asyncRecomputeProposals(String filterText) { + if (isValid()) { + control.getDisplay().asyncExec(new class Runnable { + String filterText_; + this(){filterText_=filterText;} + public void run() { + recordCursorPosition(); + recomputeProposals(filterText_); + } + }); + } else { + recomputeProposals(filterText); + } + } + + /* + * Filter the provided list of content proposals according to the filter + * text. + */ + private IContentProposal[] filterProposals( + IContentProposal[] proposals, String filterString) { + if (filterString.length is 0) { + return proposals; + } + + // Check each string for a match. Use the string displayed to the + // user, not the proposal content. + auto list = new ArraySeq!(IContentProposal); + for (int i = 0; i < proposals.length; i++) { + String string = getString(proposals[i]); + if (string.length >= filterString.length + && string.substring(0, filterString.length) + .equalsIgnoreCase(filterString)) { + list.append(proposals[i]); + } + + } + return list.toArray(); + } + + Listener getTargetControlListener() { + if (targetControlListener is null) { + targetControlListener = new TargetControlListener(); + } + return targetControlListener; + } + } + + /** + * Flag that controls the printing of debug info. + */ + public static const bool DEBUG = false; + + /** + * Indicates that a chosen proposal should be inserted into the field. + */ + public static const int PROPOSAL_INSERT = 1; + + /** + * Indicates that a chosen proposal should replace the entire contents of + * the field. + */ + public static const int PROPOSAL_REPLACE = 2; + + /** + * Indicates that the contents of the control should not be modified when a + * proposal is chosen. This is typically used when a client needs more + * specialized behavior when a proposal is chosen. In this case, clients + * typically register an IContentProposalListener so that they are notified + * when a proposal is chosen. + */ + public static const int PROPOSAL_IGNORE = 3; + + /** + * Indicates that there should be no filter applied as keys are typed in the + * popup. + */ + public static const int FILTER_NONE = 1; + + /** + * Indicates that a single character filter applies as keys are typed in the + * popup. + */ + public static const int FILTER_CHARACTER = 2; + + /** + * Indicates that a cumulative filter applies as keys are typed in the + * popup. That is, each character typed will be added to the filter. + */ + public static const int FILTER_CUMULATIVE = 3; + + /* + * Set to true to use a Table with DWT.VIRTUAL. This is a + * workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=98585#c40 + * The corresponding DWT bug is + * https://bugs.eclipse.org/bugs/show_bug.cgi?id=90321 + */ + private static const bool USE_VIRTUAL = !"motif".equals(DWT.getPlatform()); //$NON-NLS-1$ + + /* + * The delay before showing a secondary popup. + */ + private static const int POPUP_DELAY = 750; + + /* + * The character height hint for the popup. May be overridden by using + * setInitialPopupSize. + */ + private static const int POPUP_CHAR_HEIGHT = 10; + + /* + * The minimum pixel width for the popup. May be overridden by using + * setInitialPopupSize. + */ + private static const int POPUP_MINIMUM_WIDTH = 300; + + /* + * The pixel offset of the popup from the bottom corner of the control. + */ + private static const int POPUP_OFFSET = 3; + + /* + * Empty string. + */ + private static const String EMPTY = ""; //$NON-NLS-1$ + + /* + * The object that provides content proposals. + */ + private IContentProposalProvider proposalProvider; + + /* + * A label provider used to display proposals in the popup, and to extract + * Strings from non-String proposals. + */ + private ILabelProvider labelProvider; + + /* + * The control for which content proposals are provided. + */ + private Control control; + + /* + * The adapter used to extract the String contents from an arbitrary + * control. + */ + private IControlContentAdapter controlContentAdapter; + + /* + * The popup used to show proposals. + */ + private ContentProposalPopup popup; + + /* + * The keystroke that signifies content proposals should be shown. + */ + private KeyStroke triggerKeyStroke; + + /* + * The String containing characters that auto-activate the popup. + */ + private String autoActivateString; + + /* + * Integer that indicates how an accepted proposal should affect the + * control. One of PROPOSAL_IGNORE, PROPOSAL_INSERT, or PROPOSAL_REPLACE. + * Default value is PROPOSAL_INSERT. + */ + private int proposalAcceptanceStyle = PROPOSAL_INSERT; + + /* + * A bool that indicates whether key events received while the proposal + * popup is open should also be propagated to the control. Default value is + * true. + */ + private bool propagateKeys = true; + + /* + * Integer that indicates the filtering style. One of FILTER_CHARACTER, + * FILTER_CUMULATIVE, FILTER_NONE. + */ + private int filterStyle = FILTER_NONE; + + /* + * The listener we install on the control. + */ + private Listener controlListener; + + /* + * The list of IContentProposalListener listeners. + */ + private ListenerList proposalListeners; + + /* + * The list of IContentProposalListener2 listeners. + */ + private ListenerList proposalListeners2; + + /* + * Flag that indicates whether the adapter is enabled. In some cases, + * adapters may be installed but depend upon outside state. + */ + private bool isEnabled_ = true; + + /* + * The delay in milliseconds used when autoactivating the popup. + */ + private int autoActivationDelay = 0; + + /* + * A bool indicating whether a keystroke has been received. Used to see + * if an autoactivation delay was interrupted by a keystroke. + */ + private bool receivedKeyDown; + + /* + * The desired size in pixels of the proposal popup. + */ + private Point popupSize; + + /* + * The remembered position of the insertion position. Not all controls will + * restore the insertion position if the proposal popup gets focus, so we + * need to remember it. + */ + private int insertionPos = -1; + + /* + * A flag that indicates that a pending modify event was caused by + * the adapter rather than the user. + */ + private bool modifyingControlContent = false; + + /** + * Construct a content proposal adapter that can assist the user with + * choosing content for the field. + * + * @param control + * the control for which the adapter is providing content assist. + * May not be null. + * @param controlContentAdapter + * the IControlContentAdapter used to obtain and + * update the control's contents as proposals are accepted. May + * not be null. + * @param proposalProvider + * the IContentProposalProvider used to obtain + * content proposals for this control, or null if + * no content proposal is available. + * @param keyStroke + * the keystroke that will invoke the content proposal popup. If + * this value is null, then proposals will be + * activated automatically when any of the auto activation + * characters are typed. + * @param autoActivationCharacters + * An array of characters that trigger auto-activation of content + * proposal. If specified, these characters will trigger + * auto-activation of the proposal popup, regardless of whether + * an explicit invocation keyStroke was specified. If this + * parameter is null, then only a specified + * keyStroke will invoke content proposal. If this parameter is + * null and the keyStroke parameter is + * null, then all alphanumeric characters will + * auto-activate content proposal. + */ + public this(Control control, + IControlContentAdapter controlContentAdapter, + IContentProposalProvider proposalProvider, KeyStroke keyStroke, + char[] autoActivationCharacters) { + proposalListeners = new ListenerList(); + proposalListeners2 = new ListenerList(); + //super(); + // We always assume the control and content adapter are valid. + Assert.isNotNull(cast(Object)control); + Assert.isNotNull(cast(Object)controlContentAdapter); + this.control = control; + this.controlContentAdapter = controlContentAdapter; + + // The rest of these may be null + this.proposalProvider = proposalProvider; + this.triggerKeyStroke = keyStroke; + if (autoActivationCharacters.length !is 0 ) { + this.autoActivateString = autoActivationCharacters; + } + addControlListener(control); + } + + /** + * Get the control on which the content proposal adapter is installed. + * + * @return the control on which the proposal adapter is installed. + */ + public Control getControl() { + return control; + } + + /** + * Get the label provider that is used to show proposals. + * + * @return the {@link ILabelProvider} used to show proposals, or + * null if one has not been installed. + */ + public ILabelProvider getLabelProvider() { + return labelProvider; + } + + /** + * Return a bool indicating whether the receiver is enabled. + * + * @return true if the adapter is enabled, and + * false if it is not. + */ + public bool isEnabled() { + return isEnabled_; + } + + /** + * Set the label provider that is used to show proposals. The lifecycle of + * the specified label provider is not managed by this adapter. Clients must + * dispose the label provider when it is no longer needed. + * + * @param labelProvider + * the (@link ILabelProvider} used to show proposals. + */ + public void setLabelProvider(ILabelProvider labelProvider) { + this.labelProvider = labelProvider; + } + + /** + * Return the proposal provider that provides content proposals given the + * current content of the field. A value of null indicates + * that there are no content proposals available for the field. + * + * @return the {@link IContentProposalProvider} used to show proposals. May + * be null. + */ + public IContentProposalProvider getContentProposalProvider() { + return proposalProvider; + } + + /** + * Set the content proposal provider that is used to show proposals. + * + * @param proposalProvider + * the {@link IContentProposalProvider} used to show proposals + */ + public void setContentProposalProvider( + IContentProposalProvider proposalProvider) { + this.proposalProvider = proposalProvider; + } + + /** + * Return the array of characters on which the popup is autoactivated. + * + * @return An array of characters that trigger auto-activation of content + * proposal. If specified, these characters will trigger + * auto-activation of the proposal popup, regardless of whether an + * explicit invocation keyStroke was specified. If this parameter is + * null, then only a specified keyStroke will invoke + * content proposal. If this value is null and the + * keyStroke value is null, then all alphanumeric + * characters will auto-activate content proposal. + */ + public char[] getAutoActivationCharacters() { + if (autoActivateString is null) { + return null; + } + return autoActivateString/+.toCharArray()+/; + } + + /** + * Set the array of characters that will trigger autoactivation of the + * popup. + * + * @param autoActivationCharacters + * An array of characters that trigger auto-activation of content + * proposal. If specified, these characters will trigger + * auto-activation of the proposal popup, regardless of whether + * an explicit invocation keyStroke was specified. If this + * parameter is null, then only a specified + * keyStroke will invoke content proposal. If this parameter is + * null and the keyStroke value is + * null, then all alphanumeric characters will + * auto-activate content proposal. + * + */ + public void setAutoActivationCharacters(char[] autoActivationCharacters) { + if (autoActivationCharacters.length is 0) { + this.autoActivateString = null; + } else { + this.autoActivateString = autoActivationCharacters; + } + } + + /** + * Set the delay, in milliseconds, used before any autoactivation is + * triggered. + * + * @return the time in milliseconds that will pass before a popup is + * automatically opened + */ + public int getAutoActivationDelay() { + return autoActivationDelay; + + } + + /** + * Set the delay, in milliseconds, used before autoactivation is triggered. + * + * @param delay + * the time in milliseconds that will pass before a popup is + * automatically opened + */ + public void setAutoActivationDelay(int delay) { + autoActivationDelay = delay; + + } + + /** + * Get the integer style that indicates how an accepted proposal affects the + * control's content. + * + * @return a constant indicating how an accepted proposal should affect the + * control's content. Should be one of PROPOSAL_INSERT, + * PROPOSAL_REPLACE, or PROPOSAL_IGNORE. + * (Default is PROPOSAL_INSERT). + */ + public int getProposalAcceptanceStyle() { + return proposalAcceptanceStyle; + } + + /** + * Set the integer style that indicates how an accepted proposal affects the + * control's content. + * + * @param acceptance + * a constant indicating how an accepted proposal should affect + * the control's content. Should be one of + * PROPOSAL_INSERT, PROPOSAL_REPLACE, + * or PROPOSAL_IGNORE + */ + public void setProposalAcceptanceStyle(int acceptance) { + proposalAcceptanceStyle = acceptance; + } + + /** + * Return the integer style that indicates how keystrokes affect the content + * of the proposal popup while it is open. + * + * @return a constant indicating how keystrokes in the proposal popup affect + * filtering of the proposals shown. FILTER_NONE + * specifies that no filtering will occur in the content proposal + * list as keys are typed. FILTER_CUMULATIVE + * specifies that the content of the popup will be filtered by a + * string containing all the characters typed since the popup has + * been open. FILTER_CHARACTER specifies the content + * of the popup will be filtered by the most recently typed + * character. The default is FILTER_NONE. + */ + public int getFilterStyle() { + return filterStyle; + } + + /** + * Set the integer style that indicates how keystrokes affect the content of + * the proposal popup while it is open. Popup-based filtering is useful for + * narrowing and navigating the list of proposals provided once the popup is + * open. Filtering of the proposals will occur even when the control content + * is not affected by user typing. Note that automatic filtering is not used + * to achieve content-sensitive filtering such as auto-completion. Filtering + * that is sensitive to changes in the control content should be performed + * by the supplied {@link IContentProposalProvider}. + * + * @param filterStyle + * a constant indicating how keystrokes received in the proposal + * popup affect filtering of the proposals shown. + * FILTER_NONE specifies that no automatic + * filtering of the content proposal list will occur as keys are + * typed in the popup. FILTER_CUMULATIVE specifies + * that the content of the popup will be filtered by a string + * containing all the characters typed since the popup has been + * open. FILTER_CHARACTER specifies that the + * content of the popup will be filtered by the most recently + * typed character. + */ + public void setFilterStyle(int filterStyle) { + this.filterStyle = filterStyle; + } + + /** + * Return the size, in pixels, of the content proposal popup. + * + * @return a Point specifying the last width and height, in pixels, of the + * content proposal popup. + */ + public Point getPopupSize() { + return popupSize; + } + + /** + * Set the size, in pixels, of the content proposal popup. This size will be + * used the next time the content proposal popup is opened. + * + * @param size + * a Point specifying the desired width and height, in pixels, of + * the content proposal popup. + */ + public void setPopupSize(Point size) { + popupSize = size; + } + + /** + * Get the bool that indicates whether key events (including + * auto-activation characters) received by the content proposal popup should + * also be propagated to the adapted control when the proposal popup is + * open. + * + * @return a bool that indicates whether key events (including + * auto-activation characters) should be propagated to the adapted + * control when the proposal popup is open. Default value is + * true. + */ + public bool getPropagateKeys() { + return propagateKeys; + } + + /** + * Set the bool that indicates whether key events (including + * auto-activation characters) received by the content proposal popup should + * also be propagated to the adapted control when the proposal popup is + * open. + * + * @param propagateKeys + * a bool that indicates whether key events (including + * auto-activation characters) should be propagated to the + * adapted control when the proposal popup is open. + */ + public void setPropagateKeys(bool propagateKeys) { + this.propagateKeys = propagateKeys; + } + + /** + * Return the content adapter that can get or retrieve the text contents + * from the adapter's control. This method is used when a client, such as a + * content proposal listener, needs to update the control's contents + * manually. + * + * @return the {@link IControlContentAdapter} which can update the control + * text. + */ + public IControlContentAdapter getControlContentAdapter() { + return controlContentAdapter; + } + + /** + * Set the bool flag that determines whether the adapter is enabled. + * + * @param enabled + * true if the adapter is enabled and responding + * to user input, false if it is ignoring user + * input. + * + */ + public void setEnabled(bool enabled) { + // If we are disabling it while it's proposing content, close the + // content proposal popup. + if (isEnabled_ && !enabled) { + if (popup !is null) { + popup.close(); + } + } + isEnabled_ = enabled; + } + + /** + * Add the specified listener to the list of content proposal listeners that + * are notified when content proposals are chosen. + *

+ * + * @param listener + * the IContentProposalListener to be added as a listener. Must + * not be null. If an attempt is made to register + * an instance which is already registered with this instance, + * this method has no effect. + * + * @see dwtx.jface.fieldassist.IContentProposalListener + */ + public void addContentProposalListener(IContentProposalListener listener) { + proposalListeners.add(cast(Object)listener); + } + + /** + * Removes the specified listener from the list of content proposal + * listeners that are notified when content proposals are chosen. + *

+ * + * @param listener + * the IContentProposalListener to be removed as a listener. Must + * not be null. If the listener has not already + * been registered, this method has no effect. + * + * @since 3.3 + * @see dwtx.jface.fieldassist.IContentProposalListener + */ + public void removeContentProposalListener(IContentProposalListener listener) { + proposalListeners.remove(cast(Object)listener); + } + + /** + * Add the specified listener to the list of content proposal listeners that + * are notified when a content proposal popup is opened or closed. + *

+ * + * @param listener + * the IContentProposalListener2 to be added as a listener. Must + * not be null. If an attempt is made to register + * an instance which is already registered with this instance, + * this method has no effect. + * + * @since 3.3 + * @see dwtx.jface.fieldassist.IContentProposalListener2 + */ + public void addContentProposalListener(IContentProposalListener2 listener) { + proposalListeners2.add(cast(Object)listener); + } + + /** + * Remove the specified listener from the list of content proposal listeners + * that are notified when a content proposal popup is opened or closed. + *

+ * + * @param listener + * the IContentProposalListener2 to be removed as a listener. + * Must not be null. If the listener has not + * already been registered, this method has no effect. + * + * @since 3.3 + * @see dwtx.jface.fieldassist.IContentProposalListener2 + */ + public void removeContentProposalListener(IContentProposalListener2 listener) { + proposalListeners2.remove(cast(Object)listener); + } + + /* + * Add our listener to the control. Debug information to be left in until + * this support is stable on all platforms. + */ + private void addControlListener(Control control) { + if (DEBUG) { + Stdout.formatln("ContentProposalListener#installControlListener()"); //$NON-NLS-1$ + } + + if (controlListener !is null) { + return; + } + controlListener = new class Listener { + public void handleEvent(Event e) { + if (!isEnabled_) { + return; + } + + switch (e.type) { + case DWT.Traverse: + case DWT.KeyDown: + if (DEBUG) { + StringBuffer sb; + if (e.type is DWT.Traverse) { + sb = new StringBuffer("Traverse"); //$NON-NLS-1$ + } else { + sb = new StringBuffer("KeyDown"); //$NON-NLS-1$ + } + sb.append(" received by adapter"); //$NON-NLS-1$ + dump(sb.toString(), e); + } + // If the popup is open, it gets first shot at the + // keystroke and should set the doit flags appropriately. + if (popup !is null) { + popup.getTargetControlListener().handleEvent(e); + if (DEBUG) { + StringBuffer sb; + if (e.type is DWT.Traverse) { + sb = new StringBuffer("Traverse"); //$NON-NLS-1$ + } else { + sb = new StringBuffer("KeyDown"); //$NON-NLS-1$ + } + sb.append(" after being handled by popup"); //$NON-NLS-1$ + dump(sb.toString(), e); + } + + return; + } + + // We were only listening to traverse events for the popup + if (e.type is DWT.Traverse) { + return; + } + + // The popup is not open. We are looking at keydown events + // for a trigger to open the popup. + if (triggerKeyStroke !is null) { + // Either there are no modifiers for the trigger and we + // check the character field... + if ((triggerKeyStroke.getModifierKeys() is KeyStroke.NO_KEY && triggerKeyStroke + .getNaturalKey() is e.character) + || + // ...or there are modifiers, in which case the + // keycode and state must match + (triggerKeyStroke.getNaturalKey() is e.keyCode && ((triggerKeyStroke + .getModifierKeys() & e.stateMask) is triggerKeyStroke + .getModifierKeys()))) { + // We never propagate the keystroke for an explicit + // keystroke invocation of the popup + e.doit = false; + openProposalPopup(false); + return; + } + } + /* + * The triggering keystroke was not invoked. Check for + * autoactivation characters. + */ + if (e.character !is 0) { + // Auto-activation characters were specified. Check + // them. + if (autoActivateString !is null) { + if (autoActivateString.indexOf(e.character) >= 0) { + e.doit = propagateKeys; + autoActivate(); + } + } else { + // No autoactivation occurred, so record the key + // down + // as a means to interrupt any autoactivation that + // is + // pending. + receivedKeyDown = true; + } + } + break; + + // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=147377 + // Given that we will close the popup when there are no valid + // proposals, we must reopen it when there are. Normally, the + // keydown event handling will catch all the cases where it + // should reopen. But when autoactivation should occur on all + // content changes, we check it here after keys have been + // processed. + // See also https://bugs.eclipse.org/bugs/show_bug.cgi?id=183650 + // We should not autoactivate if the content change was caused + // by the popup itself. + case DWT.Modify: + if (triggerKeyStroke is null && autoActivateString is null + && !modifyingControlContent) { + if (DEBUG) { + dump("Modify event triggers autoactivation", e); //$NON-NLS-1$ + } + autoActivate(); + } + break; + default: + break; + } + } + + /** + * Dump the given events to "standard" output. + * + * @param who + * who is dumping the event + * @param e + * the event + */ + private void dump(String who, Event e) { + StringBuffer sb = new StringBuffer( + "--- [ContentProposalAdapter]\n"); //$NON-NLS-1$ + sb.append(who); + sb.append(Format(" - e: keyCode={}{}", e.keyCode, hex(e.keyCode))); //$NON-NLS-1$ + sb.append(Format("; character={}{}", e.character, hex(e.character))); //$NON-NLS-1$ + sb.append(Format("; stateMask={}{}", e.stateMask, hex(e.stateMask))); //$NON-NLS-1$ + sb.append(Format("; doit={}", e.doit)); //$NON-NLS-1$ + sb.append(Format("; detail={}", e.detail, hex(e.detail))); //$NON-NLS-1$ + sb.append(Format("; widget={}", e.widget)); //$NON-NLS-1$ + Stdout.formatln("{}",sb.toString); + } + + private String hex(int i) { + return Format("[0x{:X}]", i); //$NON-NLS-1$ + } + }; + control.addListener(DWT.KeyDown, controlListener); + control.addListener(DWT.Traverse, controlListener); + control.addListener(DWT.Modify, controlListener); + + if (DEBUG) { + Stdout.formatln("ContentProposalAdapter#installControlListener() - installed"); //$NON-NLS-1$ + } + } + + /** + * Open the proposal popup and display the proposals provided by the + * proposal provider. If there are no proposals to be shown, do not show the + * popup. This method returns immediately. That is, it does not wait for the + * popup to open or a proposal to be selected. + * + * @param autoActivated + * a bool indicating whether the popup was autoactivated. If + * false, a beep will sound when no proposals can be shown. + */ + private void openProposalPopup(bool autoActivated) { + if (isValid()) { + if (popup is null) { + // Check whether there are any proposals to be shown. + recordCursorPosition(); // must be done before getting proposals + IContentProposal[] proposals = getProposals(); + if (proposals.length > 0) { + if (DEBUG) { + Stdout.formatln("POPUP OPENED BY PRECEDING EVENT"); //$NON-NLS-1$ + } + recordCursorPosition(); + popup = new ContentProposalPopup(null, proposals); + popup.open(); + popup.getShell().addDisposeListener(new class DisposeListener { + public void widgetDisposed(DisposeEvent event) { + popup = null; + } + }); + notifyPopupOpened(); + } else if (!autoActivated) { + getControl().getDisplay().beep(); + } + } + } + } + + /** + * Open the proposal popup and display the proposals provided by the + * proposal provider. This method returns immediately. That is, it does not + * wait for a proposal to be selected. This method is used by subclasses to + * explicitly invoke the opening of the popup. If there are no proposals to + * show, the popup will not open and a beep will be sounded. + */ + protected void openProposalPopup() { + openProposalPopup(false); + } + + /** + * Close the proposal popup without accepting a proposal. This method + * returns immediately, and has no effect if the proposal popup was not + * open. This method is used by subclasses to explicitly close the popup + * based on additional logic. + * + * @since 3.3 + */ + protected void closeProposalPopup() { + if (popup !is null) { + popup.close(); + } + } + + /* + * A content proposal has been accepted. Update the control contents + * accordingly and notify any listeners. + * + * @param proposal the accepted proposal + */ + private void proposalAccepted(IContentProposal proposal) { + switch (proposalAcceptanceStyle) { + case (PROPOSAL_REPLACE): + setControlContent(proposal.getContent(), proposal + .getCursorPosition()); + break; + case (PROPOSAL_INSERT): + insertControlContent(proposal.getContent(), proposal + .getCursorPosition()); + break; + default: + // do nothing. Typically a listener is installed to handle this in + // a custom way. + break; + } + + // In all cases, notify listeners of an accepted proposal. + notifyProposalAccepted(proposal); + } + + /* + * Set the text content of the control to the specified text, setting the + * cursorPosition at the desired location within the new contents. + */ + private void setControlContent(String text, int cursorPosition) { + if (isValid()) { + // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=183650 + modifyingControlContent = true; + + controlContentAdapter.setControlContents(control, text, + cursorPosition); + + modifyingControlContent = false; + } + } + + /* + * Insert the specified text into the control content, setting the + * cursorPosition at the desired location within the new contents. + */ + private void insertControlContent(String text, int cursorPosition) { + if (isValid()) { + // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=183650 + modifyingControlContent = true; + // Not all controls preserve their selection index when they lose + // focus, so we must set it explicitly here to what it was before + // the popup opened. + // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=127108 + if (insertionPos !is -1) { + controlContentAdapter.setCursorPosition(control, insertionPos); + } + controlContentAdapter.insertControlContents(control, text, + cursorPosition); + modifyingControlContent = false; + } + } + + /* + * Check that the control and content adapter are valid. + */ + private bool isValid() { + return control !is null && !control.isDisposed() + && controlContentAdapter !is null; + } + + /* + * Record the control's cursor position. + */ + private void recordCursorPosition() { + if (isValid()) { + insertionPos = getControlContentAdapter() + .getCursorPosition(control); + + } + } + + /* + * Get the proposals from the proposal provider. Gets all of the proposals + * without doing any filtering. + */ + private IContentProposal[] getProposals() { + if (proposalProvider is null || !isValid()) { + return null; + } + if (DEBUG) { + Stdout.formatln(">>> obtaining proposals from provider"); //$NON-NLS-1$ + } + int position = insertionPos; + if (position is -1) { + position = getControlContentAdapter().getCursorPosition( + getControl()); + } + String contents = getControlContentAdapter().getControlContents( + getControl()); + IContentProposal[] proposals = proposalProvider.getProposals(contents, + position); + return proposals; + } + + /** + * Autoactivation has been triggered. Open the popup using any specified + * delay. + */ + private void autoActivate() { + if (autoActivationDelay > 0) { + auto r = new class Runnable{ + public void run(){ + receivedKeyDown = false; + try { + Thread.sleep(autoActivationDelay); + } catch (InterruptedException e) { + } + if (!isValid() || receivedKeyDown) { + return; + } + getControl().getDisplay().syncExec(new class Runnable { + public void run() { + openProposalPopup(true); + } + }); + } + }; + Thread t = new Thread(&r.run); + t.start(); + } else { + // Since we do not sleep, we must open the popup + // in an async exec. This is necessary because + // this method may be called in the middle of handling + // some event that will cause the cursor position or + // other important info to change as a result of this + // event occurring. + getControl().getDisplay().asyncExec(new class Runnable { + public void run() { + if (isValid()) { + openProposalPopup(true); + } + } + }); + } + } + + /* + * A proposal has been accepted. Notify interested listeners. + */ + private void notifyProposalAccepted(IContentProposal proposal) { + if (DEBUG) { + Stdout.formatln("Notify listeners - proposal accepted."); //$NON-NLS-1$ + } + Object[] listenerArray = proposalListeners.getListeners(); + for (int i = 0; i < listenerArray.length; i++) { + (cast(IContentProposalListener) listenerArray[i]) + .proposalAccepted(proposal); + } + } + + /* + * The proposal popup has opened. Notify interested listeners. + */ + private void notifyPopupOpened() { + if (DEBUG) { + Stdout.formatln("Notify listeners - popup opened."); //$NON-NLS-1$ + } + Object[] listenerArray = proposalListeners2.getListeners(); + for (int i = 0; i < listenerArray.length; i++) { + (cast(IContentProposalListener2) listenerArray[i]) + .proposalPopupOpened(this); + } + } + + /* + * The proposal popup has closed. Notify interested listeners. + */ + private void notifyPopupClosed() { + if (DEBUG) { + Stdout.formatln("Notify listeners - popup closed."); //$NON-NLS-1$ + } + Object[] listenerArray = proposalListeners2.getListeners(); + for (int i = 0; i < listenerArray.length; i++) { + (cast(IContentProposalListener2) listenerArray[i]) + .proposalPopupClosed(this); + } + } +} diff -r 50b0163e18f8 -r f12d40e7da8f dwtx/jface/fieldassist/ControlDecoration.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/ControlDecoration.d Thu Apr 03 18:56:20 2008 +0200 @@ -0,0 +1,1204 @@ +/******************************************************************************* + * Copyright (c) 2006, 2007 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 + * Port to the D programming language: + * Frank Benoit + *******************************************************************************/ +module dwtx.jface.fieldassist.ControlDecoration; + + +import dwt.DWT; +import dwt.events.DisposeEvent; +import dwt.events.DisposeListener; +import dwt.events.FocusEvent; +import dwt.events.FocusListener; +import dwt.events.MenuDetectEvent; +import dwt.events.MenuDetectListener; +import dwt.events.MouseAdapter; +import dwt.events.MouseEvent; +import dwt.events.MouseMoveListener; +import dwt.events.MouseTrackListener; +import dwt.events.PaintEvent; +import dwt.events.PaintListener; +import dwt.events.SelectionEvent; +import dwt.events.SelectionListener; +import dwt.graphics.GC; +import dwt.graphics.Image; +import dwt.graphics.Point; +import dwt.graphics.Rectangle; +import dwt.graphics.Region; +import dwt.widgets.Composite; +import dwt.widgets.Control; +import dwt.widgets.Display; +import dwt.widgets.Event; +import dwt.widgets.Listener; +import dwt.widgets.Shell; +import dwt.widgets.Widget; +import dwtx.core.runtime.ListenerList; + +import dwt.dwthelper.utils; +import tango.io.Stdout; + +/** + * ControlDecoration renders an image decoration near a control. It allows + * clients to specify an image and a position for the image relative to the + * control. A ControlDecoration may be assigned description text, which can + * optionally be shown when the user hovers over the image. Clients can decorate + * any kind of control. + *

+ * Decoration images always appear on the left or right side of the field, never + * above or below it. Decorations can be positioned at the top, center, or + * bottom of either side of the control. Future implementations may provide + * additional positioning options for decorations. + *

+ * ControlDecoration renders the image adjacent to the specified (already + * created) control, with no guarantee that it won't be clipped or otherwise + * obscured or overlapped by adjacent controls, including another + * ControlDecoration placed in the same location. Clients should ensure that + * there is adequate space adjacent to the control to show the decoration + * properly. + *

+ * Clients using ControlDecoration should typically ensure that enough margin + * space is reserved for a decoration by altering the layout data margins, + * although this is not assumed or required by the ControlDecoration + * implementation. + *

+ * This class is intended to be instantiated and used by clients. It is not + * intended to be subclassed by clients. + * + * @since 3.3 + * + * @see FieldDecoration + * @see FieldDecorationRegistry + */ +public class ControlDecoration { + /** + * Debug flag for tracing + */ + private static bool DEBUG = false; + + /** + * Cached platform flags for dealing with platform-specific issues. + */ + private static bool CARBON = "carbon".equals(DWT.getPlatform()); //$NON-NLS-1$ + + /** + * The associated control + */ + private Control control; + + /** + * The composite on which to render the decoration and hook mouse events, or + * null if we are hooking all parent composites. + */ + private Composite composite; + + /** + * The associated image. + */ + private Image image; + + /** + * The associated description text. + */ + private String descriptionText; + /** + * The position of the decoration. + */ + private int position; + + /** + * The decoration's visibility flag + */ + private bool visible = true; + + /** + * bool indicating whether the decoration should only be shown when the + * control has focus + */ + private bool showOnlyOnFocus = false; + + /** + * bool indicating whether the decoration should show its description + * text in a hover when the user hovers over the decoration. + */ + private bool showHover = true; + + /** + * Margin width used between the decorator and the control. + */ + private int marginWidth = 0; + + /** + * Registered selection listeners. + */ + private ListenerList selectionListeners; + + /** + * Registered menu detect listeners. + */ + private ListenerList menuDetectListeners; + + /** + * The focus listener + */ + private FocusListener focusListener; + + /** + * The dispose listener + */ + private DisposeListener disposeListener; + + /** + * The paint listener installed for drawing the decoration + */ + private PaintListener paintListener; + + /** + * The mouse listener installed for tracking the hover + */ + private MouseTrackListener mouseTrackListener; + + /** + * The mouse move listener installed for tracking the hover + */ + private MouseMoveListener mouseMoveListener; + + /** + * The untyped listener installed for notifying external listeners + */ + private Listener compositeListener; + + /** + * Control that we last installed a move listener on. We only want one at a + * time. + */ + private Control moveListeningTarget = null; + + /** + * Debug counter used to match add and remove listeners + */ + private int listenerInstalls = 0; + + /** + * The current rectangle used for tracking mouse moves + */ + private Rectangle decorationRectangle; + + /** + * An internal flag tracking whether we have focus. We use this rather than + * isFocusControl() so that we can set the flag as soon as we get the focus + * callback, rather than having to do an asyncExec in the middle of a focus + * callback to ensure that isFocusControl() represents the outcome of the + * event. + */ + private bool hasFocus = false; + + /** + * The hover used for showing description text + */ + private Hover hover; + + /** + * The hover used to show a decoration image's description. + */ + class Hover { + private static const String EMPTY = ""; //$NON-NLS-1$ + + /** + * Offset of info hover arrow from the left or right side. + */ + private int hao = 10; + + /** + * Width of info hover arrow. + */ + private int haw = 8; + + /** + * Height of info hover arrow. + */ + private int hah = 10; + + /** + * Margin around info hover text. + */ + private int hm = 2; + + /** + * This info hover's shell. + */ + Shell hoverShell; + + /** + * The info hover text. + */ + String text = EMPTY; + + /** + * The region used to manage the shell shape + */ + Region region; + + /** + * bool indicating whether the last computed polygon location had an + * arrow on left. (true if left, false if right). + */ + bool arrowOnLeft = true; + + /* + * Create a hover parented by the specified shell. + */ + this(Shell parent) { + Display display = parent.getDisplay(); + hoverShell = new Shell(parent, DWT.NO_TRIM | DWT.ON_TOP + | DWT.NO_FOCUS); + hoverShell.setBackground(display + .getSystemColor(DWT.COLOR_INFO_BACKGROUND)); + hoverShell.setForeground(display + .getSystemColor(DWT.COLOR_INFO_FOREGROUND)); + hoverShell.addPaintListener(new class PaintListener { + public void paintControl(PaintEvent pe) { + pe.gc.drawText(text, hm, hm); + if (!CARBON) { + pe.gc.drawPolygon(getPolygon(true)); + } + } + }); + hoverShell.addMouseListener(new class MouseAdapter { + public void mouseDown(MouseEvent e) { + hideHover(); + } + }); + } + + /* + * Compute a polygon that represents a hover with an arrow pointer. If + * border is true, compute the polygon inset by 1-pixel border. Consult + * the arrowOnLeft flag to determine which side the arrow is on. + */ + int[] getPolygon(bool border) { + Point e = getExtent(); + int b = border ? 1 : 0; + if (arrowOnLeft) { + return [ 0, 0, e.x - b, 0, e.x - b, e.y - b, + hao + haw, e.y - b, hao + haw / 2, e.y + hah - b, hao, + e.y - b, 0, e.y - b, 0, 0 ]; + } + return [ 0, 0, e.x - b, 0, e.x - b, e.y - b, + e.x - hao - b, e.y - b, e.x - hao - haw / 2, e.y + hah - b, + e.x - hao - haw, e.y - b, 0, e.y - b, 0, 0 ]; + } + + /* + * Dispose the hover, it is no longer needed. Dispose any resources + * allocated by the hover. + */ + void dispose() { + if (!hoverShell.isDisposed()) { + hoverShell.dispose(); + } + if (region !is null) { + region.dispose(); + } + } + + /* + * Set the visibility of the hover. + */ + void setVisible(bool visible) { + if (visible) { + if (!hoverShell.isVisible()) { + hoverShell.setVisible(true); + } + } else { + if (hoverShell.isVisible()) { + hoverShell.setVisible(false); + } + } + } + + /* + * Set the text of the hover to the specified text. Recompute the size + * and location of the hover to hover near the decoration rectangle, + * pointing the arrow toward the target control. + */ + void setText(String t, Rectangle decorationRectangle, + Control targetControl) { + if (t is null) { + t = EMPTY; + } + if (!t.equals(text)) { + Point oldSize = getExtent(); + text = t; + hoverShell.redraw(); + Point newSize = getExtent(); + if (!oldSize.opEquals(newSize)) { + // set a flag that indicates the direction of arrow + arrowOnLeft = decorationRectangle.x <= targetControl + .getLocation().x; + setNewShape(); + } + } + + Point extent = getExtent(); + int y = -extent.y - hah + 1; + int x = arrowOnLeft ? -hao + haw / 2 : -extent.x + hao + haw / 2; + + hoverShell.setLocation(control.getParent().toDisplay( + decorationRectangle.x + x, decorationRectangle.y + y)); + } + + /* + * Return whether or not the hover (shell) is visible. + */ + bool isVisible() { + return hoverShell.isVisible(); + } + + /* + * Compute the extent of the hover for the current text. + */ + Point getExtent() { + GC gc = new GC(hoverShell); + Point e = gc.textExtent(text); + gc.dispose(); + e.x += hm * 2; + e.y += hm * 2; + return e; + } + + /* + * Compute a new shape for the hover shell. + */ + void setNewShape() { + Region oldRegion = region; + region = new Region(); + region.add(getPolygon(false)); + hoverShell.setRegion(region); + if (oldRegion !is null) { + oldRegion.dispose(); + } + + } + } + + /** + * Construct a ControlDecoration for decorating the specified control at the + * specified position relative to the control. Render the decoration on top + * of any Control that happens to appear at the specified location. + *

+ * DWT constants are used to specify the position of the decoration relative + * to the control. The position should include style bits describing both + * the vertical and horizontal orientation. DWT.LEFT and + * DWT.RIGHT describe the horizontal placement of the + * decoration relative to the control, and the constants + * DWT.TOP, DWT.CENTER, and + * DWT.BOTTOM describe the vertical alignment of the + * decoration relative to the control. Decorations always appear on either + * the left or right side of the control, never above or below it. For + * example, a decoration appearing on the left side of the field, at the + * top, is specified as DWT.LEFT | DWT.TOP. If no position style bits are + * specified, the control decoration will be positioned to the left and + * center of the control (DWT.LEFT | DWT.CENTER). + *

+ * + * @param control + * the control to be decorated + * @param position + * bit-wise or of position constants (DWT.TOP, + * DWT.BOTTOM, DWT.LEFT, + * DWT.RIGHT, and DWT.CENTER). + */ + public this(Control control, int position) { + this(control, position, null); + + } + + /** + * Construct a ControlDecoration for decorating the specified control at the + * specified position relative to the control. Render the decoration only on + * the specified Composite or its children. The decoration will be clipped + * if it does not appear within the visible bounds of the composite or its + * child composites. + *

+ * DWT constants are used to specify the position of the decoration relative + * to the control. The position should include style bits describing both + * the vertical and horizontal orientation. DWT.LEFT and + * DWT.RIGHT describe the horizontal placement of the + * decoration relative to the control, and the constants + * DWT.TOP, DWT.CENTER, and + * DWT.BOTTOM describe the vertical alignment of the + * decoration relative to the control. Decorations always appear on either + * the left or right side of the control, never above or below it. For + * example, a decoration appearing on the left side of the field, at the + * top, is specified as DWT.LEFT | DWT.TOP. If no position style bits are + * specified, the control decoration will be positioned to the left and + * center of the control (DWT.LEFT | DWT.CENTER). + *

+ * + * @param control + * the control to be decorated + * @param position + * bit-wise or of position constants (DWT.TOP, + * DWT.BOTTOM, DWT.LEFT, + * DWT.RIGHT, and DWT.CENTER). + * @param composite + * The DWT composite within which the decoration should be + * rendered. The decoration will be clipped to this composite, + * but it may be rendered on a child of the composite. The + * decoration will not be visible if the specified composite or + * its child composites are not visible in the space relative to + * the control, where the decoration is to be rendered. If this + * value is null, then the decoration will be + * rendered on whichever composite (or composites) are located in + * the specified position. + */ + public this(Control control, int position, Composite composite) { + selectionListeners = new ListenerList(); + menuDetectListeners = new ListenerList(); + this.position = position; + this.control = control; + this.composite = composite; + + addControlListeners(); + + } + + /** + * Adds the listener to the collection of listeners who will be notified + * when the platform-specific context menu trigger has occurred, by sending + * it one of the messages defined in the MenuDetectListener + * interface. + *

+ * The widget field in the SelectionEvent will contain the + * Composite on which the decoration is rendered that received the click. + * The x and y fields will be in coordinates + * relative to the display. The data field will contain the + * decoration that received the event. + *

+ * + * @param listener + * the listener which should be notified + * + * @see dwt.events.MenuDetectListener + * @see dwt.events.MenuDetectEvent + * @see #removeMenuDetectListener + */ + public void addMenuDetectListener(MenuDetectListener listener) { + menuDetectListeners.add(cast(Object)listener); + } + + /** + * Removes the listener from the collection of listeners who will be + * notified when the platform-specific context menu trigger has occurred. + * + * @param listener + * the listener which should no longer be notified. This message + * has no effect if the listener was not previously added to the + * receiver. + * + * @see dwt.events.MenuDetectListener + * @see #addMenuDetectListener + */ + public void removeMenuDetectListener(MenuDetectListener listener) { + menuDetectListeners.remove(cast(Object)listener); + } + + /** + * Adds the listener to the collection of listeners who will be notified + * when the decoration is selected, by sending it one of the messages + * defined in the SelectionListener interface. + *

+ * widgetSelected is called when the decoration is selected + * (by mouse click). widgetDefaultSelected is called when the + * decoration is double-clicked. + *

+ *

+ * The widget field in the SelectionEvent will contain the + * Composite on which the decoration is rendered that received the click. + * The x and y fields will be in coordinates + * relative to that widget. The data field will contain the + * decoration that received the event. + *

+ * + * @param listener + * the listener which should be notified + * + * @see dwt.events.SelectionListener + * @see dwt.events.SelectionEvent + * @see #removeSelectionListener + */ + public void addSelectionListener(SelectionListener listener) { + selectionListeners.add(cast(Object)listener); + } + + /** + * Removes the listener from the collection of listeners who will be + * notified when the decoration is selected. + * + * @param listener + * the listener which should no longer be notified. This message + * has no effect if the listener was not previously added to the + * receiver. + * + * @see dwt.events.SelectionListener + * @see #addSelectionListener + */ + public void removeSelectionListener(SelectionListener listener) { + selectionListeners.remove(cast(Object)listener); + } + + /** + * Dispose this ControlDecoration. Unhook any listeners that have been + * installed on the target control. This method has no effect if the + * receiver is already disposed. + */ + public void dispose() { + if (control is null) { + return; + } + if (hover !is null) { + hover.dispose(); + hover = null; + } + removeControlListeners(); + control = null; + } + + /** + * Get the control that is decorated by the receiver. + * + * @return the Control decorated by the receiver. May be null + * if the control has been uninstalled. + */ + public Control getControl() { + return control; + } + + /** + * Add any listeners needed on the target control and on the composite where + * the decoration is to be rendered. + */ + private void addControlListeners() { + disposeListener = new class DisposeListener { + public void widgetDisposed(DisposeEvent event) { + dispose(); + } + }; + printAddListener(control, "DISPOSE"); //$NON-NLS-1$ + control.addDisposeListener(disposeListener); + + focusListener = new class FocusListener { + public void focusGained(FocusEvent event) { + hasFocus = true; + if (showOnlyOnFocus) { + update(); + } + } + + public void focusLost(FocusEvent event) { + hasFocus = false; + if (showOnlyOnFocus) { + update(); + } + } + }; + printAddListener(control, "FOCUS"); //$NON-NLS-1$ + control.addFocusListener(focusListener); + + // Listener for painting the decoration + paintListener = new class PaintListener { + public void paintControl(PaintEvent event) { + Control control = cast(Control) event.widget; + Rectangle rect = getDecorationRectangle(control); + if (shouldShowDecoration()) { + event.gc.drawImage(getImage(), rect.x, rect.y); + } + } + }; + + // Listener for tracking the end of a hover. Only installed + // after a hover begins. + mouseMoveListener = new class MouseMoveListener { + public void mouseMove(MouseEvent event) { + if (showHover) { + if (!decorationRectangle.contains(event.x, event.y)) { + hideHover(); + // No need to listen any longer + printRemoveListener(event.widget, "MOUSEMOVE"); //$NON-NLS-1$ + (cast(Control) event.widget) + .removeMouseMoveListener(mouseMoveListener); + moveListeningTarget = null; + } + } + } + }; + + // Listener for tracking the beginning of a hover. Always installed. + mouseTrackListener = new class MouseTrackListener { + public void mouseExit(MouseEvent event) { + // Just in case we didn't catch it before. + Control target = cast(Control) event.widget; + if (target is moveListeningTarget) { + printRemoveListener(target, "MOUSEMOVE"); //$NON-NLS-1$ + target.removeMouseMoveListener(mouseMoveListener); + moveListeningTarget = null; + } + hideHover(); + } + + public void mouseHover(MouseEvent event) { + if (showHover) { + decorationRectangle = getDecorationRectangle(cast(Control) event.widget); + if (decorationRectangle.contains(event.x, event.y)) { + showHoverText(getDescriptionText()); + Control target = cast(Control) event.widget; + if (moveListeningTarget is null) { + printAddListener(target, "MOUSEMOVE"); //$NON-NLS-1$ + target.addMouseMoveListener(mouseMoveListener); + moveListeningTarget = target; + } else if (target !is moveListeningTarget) { + printRemoveListener(moveListeningTarget, + "MOUSEMOVE"); //$NON-NLS-1$ + moveListeningTarget + .removeMouseMoveListener(mouseMoveListener); + printAddListener(target, "MOUSEMOVE"); //$NON-NLS-1$ + target.addMouseMoveListener(mouseMoveListener); + moveListeningTarget = target; + } else { + // It is already installed on this control. + } + } + } + } + + public void mouseEnter(MouseEvent event) { + // Nothing to do until a hover occurs. + } + }; + + compositeListener = new class Listener { + public void handleEvent(Event event) { + // Don't forward events if decoration is not showing + if (!visible) { + return; + } + // Notify listeners if any are registered. + switch (event.type) { + case DWT.MouseDown: + if (!selectionListeners.isEmpty()) + notifySelectionListeners(event); + break; + case DWT.MouseDoubleClick: + if (!selectionListeners.isEmpty()) + notifySelectionListeners(event); + break; + case DWT.MenuDetect: + if (!menuDetectListeners.isEmpty()) + notifyMenuDetectListeners(event); + break; + } + } + }; + + // We do not know which parent in the control hierarchy + // is providing the decoration space, so hook all the way up, until + // the shell or the specified parent composite is reached. + Composite c = control.getParent(); + while (c !is null) { + installCompositeListeners(c); + if (composite !is null && composite is c) { + // We just installed on the specified composite, so stop. + c = null; + } else if (cast(Shell)c ) { + // We just installed on a shell, so don't go further + c = null; + } else { + c = c.getParent(); + } + } + // force a redraw of the decoration area so our paint listener + // is notified. + update(); + } + + /* + * Install the listeners used to paint and track mouse events on the + * composite. + */ + private void installCompositeListeners(Composite c) { + if (!c.isDisposed()) { + printAddListener(c, "PAINT"); //$NON-NLS-1$ + c.addPaintListener(paintListener); + printAddListener(c, "MOUSETRACK"); //$NON-NLS-1$ + c.addMouseTrackListener(mouseTrackListener); + printAddListener(c, "DWT.MenuDetect"); //$NON-NLS-1$ + c.addListener(DWT.MenuDetect, compositeListener); + printAddListener(c, "DWT.MouseDown"); //$NON-NLS-1$ + c.addListener(DWT.MouseDown, compositeListener); + printAddListener(c, "DWT.MouseDoubleClick"); //$NON-NLS-1$ + c.addListener(DWT.MouseDoubleClick, compositeListener); + } + } + + /* + * Remove the listeners used to paint and track mouse events on the + * composite. + */ + private void removeCompositeListeners(Composite c) { + if (!c.isDisposed()) { + printRemoveListener(c, "PAINT"); //$NON-NLS-1$ + c.removePaintListener(paintListener); + printRemoveListener(c, "MOUSETRACK"); //$NON-NLS-1$ + c.removeMouseTrackListener(mouseTrackListener); + printRemoveListener(c, "DWT.MenuDetect"); //$NON-NLS-1$ + c.removeListener(DWT.MenuDetect, compositeListener); + printRemoveListener(c, "DWT.MouseDown"); //$NON-NLS-1$ + c.removeListener(DWT.MouseDown, compositeListener); + printRemoveListener(c, "DWT.MouseDoubleClick"); //$NON-NLS-1$ + c.removeListener(DWT.MouseDoubleClick, compositeListener); + } + } + + private void notifySelectionListeners(Event event) { + if (!(cast(Control)event.widget )) { + return; + } + if (getDecorationRectangle(cast(Control) event.widget).contains(event.x, + event.y)) { + SelectionEvent clientEvent = new SelectionEvent(event); + clientEvent.data = this; + if (getImage() !is null) { + clientEvent.height = getImage().getBounds().height; + clientEvent.width = getImage().getBounds().width; + } + Object[] listeners; + switch (event.type) { + case DWT.MouseDoubleClick: + if (event.button is 1) { + listeners = selectionListeners.getListeners(); + for (int i = 0; i < listeners.length; i++) { + (cast(SelectionListener) listeners[i]) + .widgetDefaultSelected(clientEvent); + } + } + break; + case DWT.MouseDown: + if (event.button is 1) { + listeners = selectionListeners.getListeners(); + for (int i = 0; i < listeners.length; i++) { + (cast(SelectionListener) listeners[i]) + .widgetSelected(clientEvent); + } + } + break; + } + } + } + + private void notifyMenuDetectListeners(Event event) { + if (getDecorationRectangle(null).contains(event.x, event.y)) { + MenuDetectEvent clientEvent = new MenuDetectEvent(event); + clientEvent.data = this; + Object[] listeners = menuDetectListeners.getListeners(); + for (int i = 0; i < listeners.length; i++) { + (cast(MenuDetectListener) listeners[i]).menuDetected(clientEvent); + + } + } + } + + /** + * Show the specified text using the same hover dialog as is used to show + * decorator descriptions. When {@link #setShowHover(bool)} has been set + * to true, a decoration's description text will be shown in + * an info hover over the field's control whenever the mouse hovers over the + * decoration. This method can be used to show a decoration's description + * text at other times (such as when the control receives focus), or to show + * other text associated with the field. + * + * @param text + * the text to be shown in the info hover, or null + * if no text should be shown. + */ + public void showHoverText(String text) { + if (control is null) { + return; + } + showHoverText(text, control); + } + + /** + * Hide any hover popups that are currently showing on the control. When + * {@link #setShowHover(bool)} has been set to true, a + * decoration's description text will be shown in an info hover over the + * field's control as long as the mouse hovers over the decoration, and will + * be hidden when the mouse exits the decoration. This method can be used to + * hide a hover, whether it was shown explicitly using + * {@link #showHoverText(String)}, or was showing because the user was + * hovering in the decoration. + *

+ * This message has no effect if there is no current hover. + * + */ + public void hideHover() { + if (hover !is null) { + hover.setVisible(false); + } + } + + /** + * Show the control decoration. This message has no effect if the decoration + * is already showing. If {@link #setShowOnlyOnFocus(bool)} is set to + * true, the decoration will only be shown if the control + * has focus. + */ + public void show() { + if (!visible) { + visible = true; + update(); + } + } + + /** + * Hide the control decoration. This message has no effect if the decoration + * is already hidden. + */ + public void hide() { + if (visible) { + visible = false; + update(); + } + } + + /** + * Get the description text that may be shown in a hover for this + * decoration. + * + * @return the text to be shown as a description for the decoration, or + * null if none has been set. + */ + public String getDescriptionText() { + return descriptionText; + } + + /** + * Set the image shown in this control decoration. Update the rendered + * decoration. + * + * @param text + * the text to be shown as a description for the decoration, or + * null if none has been set. + */ + public void setDescriptionText(String text) { + this.descriptionText = text; + update(); + } + + /** + * Get the image shown in this control decoration. + * + * @return the image to be shown adjacent to the control, or + * null if one has not been set. + */ + public Image getImage() { + return image; + } + + /** + * Set the image shown in this control decoration. Update the rendered + * decoration. + * + * @param image + * the image to be shown adjacent to the control + */ + public void setImage(Image image) { + this.image = image; + update(); + } + + /** + * Get the bool that controls whether the decoration is shown only when + * the control has focus. The default value of this setting is + * false. + * + * @return true if the decoration should only be shown when + * the control has focus, and false if it should + * always be shown. Note that if the control is not capable of + * receiving focus (DWT.NO_FOCUS), then the + * decoration will never show when this value is true. + */ + public bool getShowOnlyOnFocus() { + return showOnlyOnFocus; + } + + /** + * Set the bool that controls whether the decoration is shown only when + * the control has focus. The default value of this setting is + * false. + * + * @param showOnlyOnFocus + * true if the decoration should only be shown + * when the control has focus, and false if it + * should always be shown. Note that if the control is not + * capable of receiving focus (DWT.NO_FOCUS), + * then the decoration will never show when this value is + * true. + */ + public void setShowOnlyOnFocus(bool showOnlyOnFocus) { + this.showOnlyOnFocus = showOnlyOnFocus; + update(); + } + + /** + * Get the bool that controls whether the decoration's description text + * should be shown in a hover when the user hovers over the decoration. The + * default value of this setting is true. + * + * @return true if a hover popup containing the decoration's + * description text should be shown when the user hovers over the + * decoration, and false if a hover should not be + * shown. + */ + public bool getShowHover() { + return showHover; + } + + /** + * Set the bool that controls whether the decoration's description text + * should be shown in a hover when the user hovers over the decoration. The + * default value of this setting is true. + * + * @param showHover + * true if a hover popup containing the + * decoration's description text should be shown when the user + * hovers over the decoration, and false if a + * hover should not be shown. + */ + public void setShowHover(bool showHover) { + this.showHover = showHover; + update(); + } + + /** + * Get the margin width in pixels that should be used between the decorator + * and the horizontal edge of the control. The default value of this setting + * is 0. + * + * @return the number of pixels that should be reserved between the + * horizontal edge of the control and the adjacent edge of the + * decoration. + */ + public int getMarginWidth() { + return marginWidth; + } + + /** + * Set the margin width in pixels that should be used between the decorator + * and the horizontal edge of the control. The default value of this setting + * is 0. + * + * @param marginWidth + * the number of pixels that should be reserved between the + * horizontal edge of the control and the adjacent edge of the + * decoration. + */ + public void setMarginWidth(int marginWidth) { + this.marginWidth = marginWidth; + update(); + } + + /** + * Something has changed, requiring redraw. Redraw the decoration and update + * the hover text if appropriate. + */ + protected void update() { + if (control is null || control.isDisposed()) { + return; + } + Rectangle rect = getDecorationRectangle(control.getShell()); + // Redraw this rectangle in all children + control.getShell() + .redraw(rect.x, rect.y, rect.width, rect.height, true); + control.getShell().update(); + if (hover !is null && getDescriptionText() !is null) { + hover.setText(getDescriptionText(), getDecorationRectangle(control + .getParent()), control); + } + } + + /* + * Show the specified text in the hover, positioning the hover near the + * specified control. + */ + private void showHoverText(String text, Control hoverNear) { + // If we aren't to show a hover, don't do anything. + if (!showHover) { + return; + } + // If there is no text, don't do anything. + if (text is null) { + hideHover(); + return; + } + + // If there is no control, nothing to do + if (control is null) { + return; + } + // Create the hover if it's not showing + if (hover is null) { + hover = new Hover(hoverNear.getShell()); + } + hover.setText(text, getDecorationRectangle(control.getParent()), + control); + hover.setVisible(true); + } + + /* + * Remove any listeners installed on the controls. + */ + private void removeControlListeners() { + if (control is null) { + return; + } + printRemoveListener(control, "FOCUS"); //$NON-NLS-1$ + control.removeFocusListener(focusListener); + focusListener = null; + + printRemoveListener(control, "DISPOSE"); //$NON-NLS-1$ + control.removeDisposeListener(disposeListener); + disposeListener = null; + + Composite c = control.getParent(); + while (c !is null) { + removeCompositeListeners(c); + if (composite !is null && composite is c) { + // We previously installed listeners only to the specified + // composite, so stop. + c = null; + } else if (cast(Shell)c ) { + // We previously installed listeners only up to the first Shell + // encountered, so stop. + c = null; + } else { + c = c.getParent(); + } + } + paintListener = null; + mouseTrackListener = null; + compositeListener = null; + + // We may have a remaining mouse move listener installed + if (moveListeningTarget !is null) { + printRemoveListener(moveListeningTarget, "MOUSEMOVE"); //$NON-NLS-1$ + moveListeningTarget.removeMouseMoveListener(mouseMoveListener); + moveListeningTarget = null; + mouseMoveListener = null; + } + if (DEBUG) { + if (listenerInstalls > 0) { + Stdout.formatln("LISTENER LEAK>>>CHECK TRACE ABOVE"); //$NON-NLS-1$ + } else if (listenerInstalls < 0) { + Stdout.formatln("REMOVED UNREGISTERED LISTENERS>>>CHECK TRACE ABOVE"); //$NON-NLS-1$ + } else { + Stdout.formatln("ALL INSTALLED LISTENERS WERE REMOVED."); //$NON-NLS-1$ + } + } + } + + /** + * Return the rectangle in which the decoration should be rendered, in + * coordinates relative to the specified control. If the specified control + * is null, return the rectangle in display coordinates. + * + * @param targetControl + * the control whose coordinates should be used + * @return the rectangle in which the decoration should be rendered + */ + protected Rectangle getDecorationRectangle(Control targetControl) { + if (getImage() is null || control is null) { + return new Rectangle(0, 0, 0, 0); + } + // Compute the bounds first relative to the control's parent. + Rectangle imageBounds = getImage().getBounds(); + Rectangle controlBounds = control.getBounds(); + int x, y; + // Compute x + if ((position & DWT.RIGHT) is DWT.RIGHT) { + x = controlBounds.x + controlBounds.width + marginWidth; + } else { + // default is left + x = controlBounds.x - imageBounds.width - marginWidth; + } + // Compute y + if ((position & DWT.TOP) is DWT.TOP) { + y = controlBounds.y; + } else if ((position & DWT.BOTTOM) is DWT.BOTTOM) { + y = controlBounds.y + control.getBounds().height + - imageBounds.height; + } else { + // default is center + y = controlBounds.y + + (control.getBounds().height - imageBounds.height) / 2; + } + + // Now convert to coordinates relative to the target control. + Point globalPoint = control.getParent().toDisplay(x, y); + Point targetPoint; + if (targetControl is null) { + targetPoint = globalPoint; + } else { + targetPoint = targetControl.toControl(globalPoint); + } + return new Rectangle(targetPoint.x, targetPoint.y, imageBounds.width, + imageBounds.height); + } + + /* + * Return true if the decoration should be shown, false if it should not. + */ + private bool shouldShowDecoration() { + if (!visible) { + return false; + } + if (control is null || control.isDisposed() || getImage() is null) { + return false; + } + + if (!control.isVisible()) { + return false; + } + if (showOnlyOnFocus) { + return hasFocus; + } + return true; + } + + /* + * If in debug mode, print info about adding the specified listener. + */ + private void printAddListener(Widget widget, String listenerType) { + listenerInstalls++; + if (DEBUG) { + Stdout.formatln("Added listener>>>{} to>>>{}", listenerType, widget); //$NON-NLS-1$//$NON-NLS-2$ + } + } + + /* + * If in debug mode, print info about adding the specified listener. + */ + private void printRemoveListener(Widget widget, String listenerType) { + listenerInstalls--; + if (DEBUG) { + Stdout.formatln("Removed listener>>>{} from>>>{}", listenerType, widget); //$NON-NLS-1$//$NON-NLS-2$ + } + } +} diff -r 50b0163e18f8 -r f12d40e7da8f dwtx/jface/fieldassist/FieldAssistColors.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/FieldAssistColors.d Thu Apr 03 18:56:20 2008 +0200 @@ -0,0 +1,222 @@ +/******************************************************************************* + * Copyright (c) 2006, 2007 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 + * Port to the D programming language: + * Frank Benoit + *******************************************************************************/ +module dwtx.jface.fieldassist.FieldAssistColors; + +import tango.util.collection.ArraySeq; +import tango.util.collection.HashMap; +import tango.util.collection.model.Map; +import tango.util.collection.model.Seq; + +import dwt.DWT; +import dwt.graphics.Color; +import dwt.graphics.RGB; +import dwt.widgets.Control; +import dwt.widgets.Display; +import dwtx.jface.resource.JFaceColors; + +import dwt.dwthelper.utils; +import dwt.dwthelper.Runnable; +import tango.io.Stdout; + +/** + * FieldAssistColors defines protocol for retrieving colors that can be used to + * provide visual cues with fields. For consistency with JFace dialogs and + * wizards, it is recommended that FieldAssistColors is used when colors are + * used to annotate fields. + *

+ * Color resources that are returned using methods in this class are maintained + * in the JFace color registries, or by DWT. Users of any color resources + * provided by this class are not responsible for the lifecycle of the color. + * Colors provided by this class should never be disposed by clients. In some + * cases, clients are provided information, such as RGB values, in order to + * create their own color resources. In these cases, the client should manage + * the lifecycle of any created resource. + * + * @since 3.2 + * @deprecated As of 3.3, this class is no longer necessary. + */ +public class FieldAssistColors { + + private static bool DEBUG = false; + + /* + * Keys are background colors, values are the color with the alpha value + * applied + */ + private static Map!(Object,Object) requiredFieldColorMap; + + /* + * Keys are colors we have created, values are the displays on which they + * were created. + */ + private static Map!(Object,Object) displays; + + static this(){ + requiredFieldColorMap = new HashMap!(Object,Object); + displays = new HashMap!(Object,Object); + } + + /** + * Compute the RGB of the color that should be used for the background of a + * control to indicate that the control has an error. Because the color + * suitable for indicating an error depends on the colors set into the + * control, this color is always computed dynamically and provided as an RGB + * value. Clients who use this RGB to create a Color resource are + * responsible for managing the life cycle of the color. + *

+ * This color is computed dynamically each time that it is queried. Clients + * should typically call this method once, create a color from the RGB + * provided, and dispose of the color when finished using it. + * + * @param control + * the control for which the background color should be computed. + * @return the RGB value indicating a background color appropriate for + * indicating an error in the control. + */ + public static RGB computeErrorFieldBackgroundRGB(Control control) { + /* + * Use a 10% alpha of the error color applied on top of the widget + * background color. + */ + Color dest = control.getBackground(); + Color src = JFaceColors.getErrorText(control.getDisplay()); + int destRed = dest.getRed(); + int destGreen = dest.getGreen(); + int destBlue = dest.getBlue(); + + // 10% alpha + int alpha = cast(int) (0xFF * 0.10f); + // Alpha blending math + destRed += (src.getRed() - destRed) * alpha / 0xFF; + destGreen += (src.getGreen() - destGreen) * alpha / 0xFF; + destBlue += (src.getBlue() - destBlue) * alpha / 0xFF; + + return new RGB(destRed, destGreen, destBlue); + } + + /** + * Return the color that should be used for the background of a control to + * indicate that the control is a required field and does not have content. + *

+ * This color is managed by FieldAssistResources and should never be + * disposed by clients. + * + * @param control + * the control on which the background color will be used. + * @return the color used to indicate that a field is required. + */ + public static Color getRequiredFieldBackgroundColor(Control control) { + final Display display = control.getDisplay(); + + // If we are in high contrast mode, then don't apply an alpha + if (display.getHighContrast()) { + return control.getBackground(); + } + + // See if a color has already been computed + Object storedColor = requiredFieldColorMap.get(control.getBackground()); + if (storedColor !is null) { + return cast(Color) storedColor; + } + + // There is no color already created, so we must create one. + // Use a 15% alpha of yellow on top of the widget background. + Color dest = control.getBackground(); + Color src = display.getSystemColor(DWT.COLOR_YELLOW); + int destRed = dest.getRed(); + int destGreen = dest.getGreen(); + int destBlue = dest.getBlue(); + + // 15% alpha + int alpha = cast(int) (0xFF * 0.15f); + // Alpha blending math + destRed += (src.getRed() - destRed) * alpha / 0xFF; + destGreen += (src.getGreen() - destGreen) * alpha / 0xFF; + destBlue += (src.getBlue() - destBlue) * alpha / 0xFF; + + // create the color + Color color = new Color(display, destRed, destGreen, destBlue); + // record the color in a map using the original color as the key + requiredFieldColorMap.add(dest, color); + // If we have never created a color on this display before, install + // a dispose exec on the display. + if (!displays.contains(display)) { + display.disposeExec(new class Runnable { + public void run() { + disposeColors(display); + } + }); + } + // Record the color and its display in a map for later disposal. + displays.add(color, display); + return color; + } + + /* + * Dispose any colors that were allocated for the given display. + */ + private static void disposeColors(Display display) { + auto toBeRemoved = new ArraySeq!(Object); + + if (DEBUG) { + Stdout.formatln("Display map is {}", (cast(Object)displays).toString()); //$NON-NLS-1$ + Stdout.formatln("Color map is {}", (cast(Object)requiredFieldColorMap).toString()); //$NON-NLS-1$ + } + + // Look for any stored colors that were created on this display + foreach( k, v; displays ){ + Color color = cast(Color) k; + if ((cast(Display) displays.get(color)).opEquals(display)) { + // The color is on this display. Mark it for removal. + toBeRemoved.append(color); + + // Now look for any references to it in the required field color + // map + auto toBeRemovedFromRequiredMap = new ArraySeq!(Object); + foreach( k, v; requiredFieldColorMap ){ + Color bgColor = cast(Color) k; + if ((cast(Color) requiredFieldColorMap.get(bgColor)) + .opEquals(color)) { + // mark it for removal from the required field color map + toBeRemovedFromRequiredMap.append(bgColor); + } + } + // Remove references in the required field map now that + // we are done iterating. + for (int j = 0; j < toBeRemovedFromRequiredMap.size(); j++) { + requiredFieldColorMap.remove(toBeRemovedFromRequiredMap + .get(j)); + } + } + } + // Remove references in the display map now that we are + // done iterating + for (int i = 0; i < toBeRemoved.size(); i++) { + Color color = cast(Color) toBeRemoved.get(i); + // Removing from the display map must be done before disposing the + // color or else the comparison between this color and the one + // in the map will fail. + displays.remove(color); + // Dispose it + if (DEBUG) { + Stdout.formatln("Disposing color {}", color.toString()); //$NON-NLS-1$ + } + color.dispose(); + } + if (DEBUG) { + Stdout.formatln("Display map is {}", (cast(Object)displays).toString()); //$NON-NLS-1$ + Stdout.formatln("Color map is {}", (cast(Object)requiredFieldColorMap).toString()); //$NON-NLS-1$ + } + } + +} diff -r 50b0163e18f8 -r f12d40e7da8f dwtx/jface/fieldassist/IContentProposal.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/IContentProposal.d Thu Apr 03 18:56:20 2008 +0200 @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) 2005, 2006 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 + * Port to the D programming language: + * Frank Benoit + *******************************************************************************/ +module dwtx.jface.fieldassist.IContentProposal; + +import dwt.dwthelper.utils; + +/** + * IContentProposal describes a content proposal to be shown. It consists of the + * content that will be provided if the proposal is accepted, an optional label + * used to describe the content to the user, and an optional description that + * further elaborates the meaning of the proposal. + * + * @since 3.2 + */ +public interface IContentProposal { + /** + * Return the content represented by this proposal. + * + * @return the String content represented by this proposal. + */ + public String getContent(); + + /** + * Return the integer position within the contents that the cursor should be + * placed after the proposal is accepted. + * + * @return the zero-based index position within the contents where the + * cursor should be placed after the proposal is accepted. + */ + public int getCursorPosition(); + + /** + * Return the label used to describe this proposal. + * + * @return the String label used to display the proposal. If + * null, then the content will be displayed as the + * label. + */ + public String getLabel(); + + /** + * Return a description that describes this proposal. + * + * @return the String label used to further the proposal. If + * null, then no description will be displayed. + */ + public String getDescription(); + +} diff -r 50b0163e18f8 -r f12d40e7da8f dwtx/jface/fieldassist/IContentProposalListener.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/IContentProposalListener.d Thu Apr 03 18:56:20 2008 +0200 @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 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 + * Port to the D programming language: + * Frank Benoit + *******************************************************************************/ +module dwtx.jface.fieldassist.IContentProposalListener; + +import dwtx.jface.fieldassist.IContentProposal; + +/** + * This interface is used to listen to notifications from a + * {@link ContentProposalAdapter}. + * + * @since 3.2 + */ +public interface IContentProposalListener { + /** + * A content proposal has been accepted. + * + * @param proposal + * the accepted content proposal + */ + public void proposalAccepted(IContentProposal proposal); +} diff -r 50b0163e18f8 -r f12d40e7da8f dwtx/jface/fieldassist/IContentProposalListener2.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/IContentProposalListener2.d Thu Apr 03 18:56:20 2008 +0200 @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2006 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 + * Port to the D programming language: + * Frank Benoit + *******************************************************************************/ +module dwtx.jface.fieldassist.IContentProposalListener2; + +import dwtx.jface.fieldassist.ContentProposalAdapter; + +import dwt.dwthelper.utils; + +/** + * This interface is used to listen to additional notifications from a + * {@link ContentProposalAdapter}. + * + * @since 3.3 + */ +public interface IContentProposalListener2 { + /** + * A content proposal popup has been opened for content proposal assistance. + * + * @param adapter + * the ContentProposalAdapter which is providing content proposal + * behavior to a control + */ + public void proposalPopupOpened(ContentProposalAdapter adapter); + + /** + * A content proposal popup has been closed. + * + * @param adapter + * the ContentProposalAdapter which is providing content proposal + * behavior to a control + */ + public void proposalPopupClosed(ContentProposalAdapter adapter); +} diff -r 50b0163e18f8 -r f12d40e7da8f dwtx/jface/fieldassist/IContentProposalProvider.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/IContentProposalProvider.d Thu Apr 03 18:56:20 2008 +0200 @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2005, 2006 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 + * Port to the D programming language: + * Frank Benoit + *******************************************************************************/ +module dwtx.jface.fieldassist.IContentProposalProvider; + +import dwtx.jface.fieldassist.IContentProposal; + +import dwt.dwthelper.utils; + +/** + * IContentProposalProvider provides an array of IContentProposals that are + * appropriate for a textual dialog field, given the field's current content and + * the current cursor position. + * + * @since 3.2 + */ +public interface IContentProposalProvider { + + /** + * Return an array of Objects representing the valid content proposals for a + * field. + * + * @param contents + * the current contents of the text field + * @param position + * the current position of the cursor in the contents + * + * @return the array of {@link IContentProposal} that represent valid + * proposals for the field. + */ + IContentProposal[] getProposals(String contents, int position); +} diff -r 50b0163e18f8 -r f12d40e7da8f dwtx/jface/fieldassist/IControlContentAdapter.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/IControlContentAdapter.d Thu Apr 03 18:56:20 2008 +0200 @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) 2005, 2006 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 + * Port to the D programming language: + * Frank Benoit + *******************************************************************************/ +module dwtx.jface.fieldassist.IControlContentAdapter; + +import dwt.graphics.Rectangle; +import dwt.widgets.Control; + +import dwt.dwthelper.utils; + +/** + * This interface is used to set and retrieve text content from an arbitrary + * control. Clients are expected to implement this interface when defining a + * {@link ContentProposalAdapter}, in order to specify how to retrieve and set + * the contents of the control being adapted. + * + * @since 3.2 + */ +public interface IControlContentAdapter { + /** + * Set the contents of the specified control to the specified text. Must not + * be null. + * + * @param control + * the control whose contents are to be set (replaced). + * @param contents + * the String specifying the new control content. + * @param cursorPosition + * the zero-based index representing the desired cursor position + * in the control's contents after the contents are set. + */ + public void setControlContents(Control control, String contents, + int cursorPosition); + + /** + * Insert the specified contents into the control's current contents. Must + * not be null. + * + * @param control + * the control whose contents are to be altered. + * @param contents + * the String to be inserted into the control contents. + * @param cursorPosition + * the zero-based index representing the desired cursor position + * within the inserted contents after the insertion is made. + */ + public void insertControlContents(Control control, String contents, + int cursorPosition); + + /** + * Get the text contents of the control. + * + * @param control + * the control whose contents are to be retrieved. + * @return the String contents of the control. + */ + public String getControlContents(Control control); + + /** + * Get the current cursor position in the control. The position is specified + * as a zero-based index into the string. Valid ranges are from 0 to N, + * where N is the size of the contents string. A value of N indicates that + * the cursor is at the end of the contents. + * + * @param control + * the control whose position is to be retrieved. + * @return the zero-based index representing the cursor position in the + * control's contents. + */ + public int getCursorPosition(Control control); + + /** + * Get the bounds (in pixels) of the insertion point for the control + * content. This is a rectangle, in coordinates relative to the control, + * where the insertion point is displayed. If the implementer does not have + * an insertion point, or cannot determine the location of the insertion + * point, it is appropriate to return the bounds of the entire control. This + * value may be used to position a content proposal popup. + * + * @param control + * the control whose offset is to be retrieved. + * @return the pixel width representing the distance between the edge of the + * control and the insertion point. + */ + public Rectangle getInsertionBounds(Control control); + + /** + * Set the current cursor position in the control. The position is specified + * as a zero-based index into the string. Valid ranges are from 0 to N, + * where N is the size of the contents string. A value of N indicates that + * the cursor is at the end of the contents. + * + * @param control + * the control whose cursor position is to be set. + * @param index + * the zero-based index representing the cursor position in the + * control's contents. + */ + public void setCursorPosition(Control control, int index); +} diff -r 50b0163e18f8 -r f12d40e7da8f dwtx/jface/fieldassist/SimpleContentProposalProvider.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/SimpleContentProposalProvider.d Thu Apr 03 18:56:20 2008 +0200 @@ -0,0 +1,146 @@ +/******************************************************************************* + * Copyright (c) 2005, 2006 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 + * Port to the D programming language: + * Frank Benoit + *******************************************************************************/ +module dwtx.jface.fieldassist.SimpleContentProposalProvider; + +import dwtx.jface.fieldassist.IContentProposalProvider; +import dwtx.jface.fieldassist.IContentProposal; + +import tango.util.collection.ArraySeq; + +import dwt.dwthelper.utils; + +/** + * SimpleContentProposalProvider is a class designed to map a static list of + * Strings to content proposals. + * + * @see IContentProposalProvider + * @since 3.2 + * + */ +public class SimpleContentProposalProvider : IContentProposalProvider { + + /* + * The proposals provided. + */ + private String[] proposals; + + /* + * The proposals mapped to IContentProposal. Cached for speed in the case + * where filtering is not used. + */ + private IContentProposal[] contentProposals; + + /* + * bool that tracks whether filtering is used. + */ + private bool filterProposals = false; + + /** + * Construct a SimpleContentProposalProvider whose content proposals are + * always the specified array of Objects. + * + * @param proposals + * the array of Strings to be returned whenever proposals are + * requested. + */ + public this(String[] proposals) { + //super(); + this.proposals = proposals; + } + + /** + * Return an array of Objects representing the valid content proposals for a + * field. Ignore the current contents of the field. + * + * @param contents + * the current contents of the field (only consulted if filtering + * is set to true) + * @param position + * the current cursor position within the field (ignored) + * @return the array of Objects that represent valid proposals for the field + * given its current content. + */ + public IContentProposal[] getProposals(String contents, int position) { + if (filterProposals) { + auto list = new ArraySeq!(IContentProposal); + for (int i = 0; i < proposals.length; i++) { + if (proposals[i].length > contents.length + && proposals[i].substring(0, contents.length) + .equalsIgnoreCase(contents)) { + list.append(makeContentProposal(proposals[i])); + } + } + return list.toArray(); + } + if (contentProposals is null) { + contentProposals = new IContentProposal[proposals.length]; + for (int i = 0; i < proposals.length; i++) { + contentProposals[i] = makeContentProposal(proposals[i]); + } + } + return contentProposals; + } + + /** + * Set the Strings to be used as content proposals. + * + * @param items + * the array of Strings to be used as proposals. + */ + public void setProposals(String[] items) { + this.proposals = items; + contentProposals = null; + } + + /** + * Set the bool that controls whether proposals are filtered according to + * the current field content. + * + * @param filterProposals + * true if the proposals should be filtered to + * show only those that match the current contents of the field, + * and false if the proposals should remain the + * same, ignoring the field content. + * @since 3.3 + */ + public void setFiltering(bool filterProposals) { + this.filterProposals = filterProposals; + // Clear any cached proposals. + contentProposals = null; + } + + /* + * Make an IContentProposal for showing the specified String. + */ + private IContentProposal makeContentProposal( String proposal) { + return new class IContentProposal { + String proposal_; + this(){proposal_=proposal;} + public String getContent() { + return proposal_; + } + + public String getDescription() { + return null; + } + + public String getLabel() { + return null; + } + + public int getCursorPosition() { + return proposal_.length; + } + }; + } +} diff -r 50b0163e18f8 -r f12d40e7da8f dwtx/jface/fieldassist/TextContentAdapter.d --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/fieldassist/TextContentAdapter.d Thu Apr 03 18:56:20 2008 +0200 @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) 2005, 2006 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 + * Port to the D programming language: + * Frank Benoit + *******************************************************************************/ +module dwtx.jface.fieldassist.TextContentAdapter; + +import dwtx.jface.fieldassist.IControlContentAdapter; + +import dwt.graphics.Point; +import dwt.graphics.Rectangle; +import dwt.widgets.Control; +import dwt.widgets.Text; + +import dwt.dwthelper.utils; + +/** + * An {@link IControlContentAdapter} for DWT Text controls. This is a + * convenience class for easily creating a {@link ContentProposalAdapter} for + * text fields. + * + * @since 3.2 + */ +public class TextContentAdapter : IControlContentAdapter { + + /* + * (non-Javadoc) + * + * @see dwtx.jface.dialogs.taskassistance.IControlContentAdapter#getControlContents(dwt.widgets.Control) + */ + public String getControlContents(Control control) { + return (cast(Text) control).getText(); + } + + /* + * (non-Javadoc) + * + * @see dwtx.jface.fieldassist.IControlContentAdapter#setControlContents(dwt.widgets.Control, + * java.lang.String, int) + */ + public void setControlContents(Control control, String text, + int cursorPosition) { + (cast(Text) control).setText(text); + (cast(Text) control).setSelection(cursorPosition, cursorPosition); + } + + /* + * (non-Javadoc) + * + * @see dwtx.jface.fieldassist.IControlContentAdapter#insertControlContents(dwt.widgets.Control, + * java.lang.String, int) + */ + public void insertControlContents(Control control, String text, + int cursorPosition) { + Point selection = (cast(Text) control).getSelection(); + (cast(Text) control).insert(text); + // Insert will leave the cursor at the end of the inserted text. If this + // is not what we wanted, reset the selection. + if (cursorPosition < text.length) { + (cast(Text) control).setSelection(selection.x + cursorPosition, + selection.x + cursorPosition); + } + } + + /* + * (non-Javadoc) + * + * @see dwtx.jface.fieldassist.IControlContentAdapter#getCursorPosition(dwt.widgets.Control) + */ + public int getCursorPosition(Control control) { + return (cast(Text) control).getCaretPosition(); + } + + /* + * (non-Javadoc) + * + * @see dwtx.jface.fieldassist.IControlContentAdapter#getInsertionBounds(dwt.widgets.Control) + */ + public Rectangle getInsertionBounds(Control control) { + Text text = cast(Text) control; + Point caretOrigin = text.getCaretLocation(); + return new Rectangle(caretOrigin.x, caretOrigin.y, 1, text + .getLineHeight()); + } + + /* + * (non-Javadoc) + * + * @see dwtx.jface.fieldassist.IControlContentAdapter#setCursorPosition(dwt.widgets.Control, + * int) + */ + public void setCursorPosition(Control control, int position) { + (cast(Text) control).setSelection(new Point(position, position)); + } +}