1/*******************************************************************************
2 * Copyright (c) 2011 Google, Inc.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
7 *
8 * Contributors:
9 *    Google, Inc. - initial API and implementation
10 *******************************************************************************/
11package org.eclipse.wb.internal.core.model.property.editor;
12
13import org.eclipse.jface.bindings.keys.KeyStroke;
14import org.eclipse.jface.fieldassist.ContentProposalAdapter;
15import org.eclipse.jface.fieldassist.IContentProposal;
16import org.eclipse.jface.fieldassist.IContentProposalListener;
17import org.eclipse.jface.fieldassist.IContentProposalListener2;
18import org.eclipse.jface.fieldassist.IContentProposalProvider;
19import org.eclipse.jface.fieldassist.IControlContentAdapter;
20import org.eclipse.jface.fieldassist.TextContentAdapter;
21import org.eclipse.jface.viewers.ILabelProvider;
22import org.eclipse.swt.SWT;
23import org.eclipse.swt.events.FocusEvent;
24import org.eclipse.swt.events.FocusListener;
25import org.eclipse.swt.events.KeyAdapter;
26import org.eclipse.swt.events.KeyEvent;
27import org.eclipse.swt.events.KeyListener;
28import org.eclipse.swt.graphics.Point;
29import org.eclipse.swt.graphics.Rectangle;
30import org.eclipse.swt.widgets.Display;
31import org.eclipse.swt.widgets.Event;
32import org.eclipse.swt.widgets.Listener;
33import org.eclipse.swt.widgets.Text;
34import org.eclipse.wb.internal.core.model.property.Property;
35import org.eclipse.wb.internal.core.model.property.table.PropertyTable;
36
37/**
38 * Abstract {@link PropertyEditor} for that uses {@link Text} as control.
39 *
40 * @author scheglov_ke
41 * @coverage core.model.property.editor
42 */
43public abstract class AbstractTextPropertyEditor extends TextDisplayPropertyEditor {
44  ////////////////////////////////////////////////////////////////////////////
45  //
46  // Editing
47  //
48  ////////////////////////////////////////////////////////////////////////////
49  private Text m_textControl;
50  private boolean m_ignoreFocusLost;
51
52  // BEGIN ADT MODIFICATIONS
53  // ContentProposalAdapter which exposes the openProposalPopup method such
54  // that we can open the dialog up immediately on focus gain to show all available
55  // alternatives (the default implementation requires at least one keytroke before
56  // it shows up)
57    private class ImmediateProposalAdapter extends ContentProposalAdapter
58        implements FocusListener, IContentProposalListener, IContentProposalListener2 {
59        private final PropertyTable m_propertyTable;
60        private final IContentProposalProvider m_proposalProvider;
61        public ImmediateProposalAdapter(
62                Text control,
63                IControlContentAdapter controlContentAdapter,
64                IContentProposalProvider proposalProvider,
65                KeyStroke keyStroke,
66                char[] autoActivationCharacters,
67                PropertyTable propertyTable) {
68            super(control, controlContentAdapter, proposalProvider, keyStroke,
69                    autoActivationCharacters);
70            m_propertyTable = propertyTable;
71            m_proposalProvider = proposalProvider;
72
73            // On focus gain, start completing
74            control.addFocusListener(this);
75
76            // Listen on popup open and close events, in order to disable
77            // focus handling on the textfield during those events.
78            // This is necessary since otherwise as soon as the user clicks
79            // on the popup with the mouse, the text field loses focus, and
80            // then instantly closes the popup -- without the selection being
81            // applied. See for example
82            //   http://dev.eclipse.org/viewcvs/viewvc.cgi/org.eclipse.jface.snippets/
83            //      Eclipse%20JFace%20Snippets/org/eclipse/jface/snippets/viewers/
84            //      Snippet060TextCellEditorWithContentProposal.java?view=markup
85            // for another example of this technique.
86            addContentProposalListener((IContentProposalListener) this);
87            addContentProposalListener((IContentProposalListener2) this);
88
89            /* Triggering on empty is disabled for now: it has the unfortunate side-effect
90               that it's impossible to enter a blank text field - blank matches everything,
91               so the first item will automatically be selected when you press return.
92
93
94            // If you edit the text and delete everything, the normal implementation
95            // will close the popup; we'll reopen it
96            control.addModifyListener(new ModifyListener() {
97                @Override
98                public void modifyText(ModifyEvent event) {
99                    if (((Text) getControl()).getText().isEmpty()) {
100                        openIfNecessary();
101                    }
102                }
103            });
104            */
105        }
106
107        private void openIfNecessary() {
108            if (m_textControl == null || m_textControl.isDisposed() ||
109                    m_proposalProvider.getProposals(m_textControl.getText(),
110                            m_textControl.getCaretPosition()).length == 0) {
111                return;
112            }
113
114            getControl().getDisplay().asyncExec(new Runnable() {
115                @Override
116                public void run() {
117                    if (!isProposalPopupOpen()) {
118                        openProposalPopup();
119                    }
120                }
121            });
122        }
123
124        // ---- Implements FocusListener ----
125
126        @Override
127        public void focusGained(FocusEvent event) {
128            openIfNecessary();
129        }
130
131        @Override
132        public void focusLost(FocusEvent event) {
133        }
134
135        // ---- Implements IContentProposalListener ----
136
137        @Override
138        public void proposalAccepted(IContentProposal proposal) {
139            closeProposalPopup();
140            m_propertyTable.deactivateEditor(true);
141        }
142
143        // ---- Implements IContentProposalListener2 ----
144
145        @Override
146        public void proposalPopupClosed(ContentProposalAdapter adapter) {
147            m_ignoreFocusLost = false;
148        }
149
150        @Override
151        public void proposalPopupOpened(ContentProposalAdapter adapter) {
152            m_ignoreFocusLost = true;
153        }
154    }
155  // END ADT MODIFICATIONS
156
157  @Override
158  public boolean activate(final PropertyTable propertyTable, final Property property, Point location)
159      throws Exception {
160    // create Text
161    {
162      m_textControl = new Text(propertyTable, SWT.NONE);
163
164      @SuppressWarnings("unused")
165      TextControlActionsManager manager = new TextControlActionsManager(m_textControl);
166
167      m_textControl.setEditable(isEditable());
168
169        // BEGIN ADT MODIFICATIONS
170        // Add support for field completion, if the property provides an IContentProposalProvider
171        // via its the getAdapter method.
172        IContentProposalProvider completion = property.getAdapter(IContentProposalProvider.class);
173        if (completion != null) {
174            ImmediateProposalAdapter adapter = new ImmediateProposalAdapter(
175                    m_textControl, new TextContentAdapter(), completion, null, null,
176                    propertyTable);
177            adapter.setFilterStyle(ContentProposalAdapter.FILTER_NONE);
178            adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE);
179            ILabelProvider labelProvider = property.getAdapter(ILabelProvider.class);
180            if (labelProvider != null) {
181                adapter.setLabelProvider(labelProvider);
182            }
183        }
184        // END ADT MODIFICATIONS
185      m_textControl.setFocus();
186    }
187    // add listeners
188    m_textControl.addKeyListener(new KeyAdapter() {
189      @Override
190      public void keyPressed(KeyEvent e) {
191        try {
192          handleKeyPressed(propertyTable, property, e);
193        } catch (Throwable ex) {
194          propertyTable.deactivateEditor(false);
195          propertyTable.handleException(ex);
196        }
197      }
198    });
199    m_textControl.addListener(SWT.FocusOut, new Listener() {
200      @Override
201    public void handleEvent(Event event) {
202        if (!m_ignoreFocusLost) {
203          propertyTable.deactivateEditor(true);
204        }
205      }
206    });
207    // set data
208    toWidget(property);
209    // keep us active
210    return true;
211  }
212
213  @Override
214  public final void setBounds(Rectangle bounds) {
215    m_textControl.setBounds(bounds);
216  }
217
218  @Override
219  public final void deactivate(PropertyTable propertyTable, Property property, boolean save) {
220    if (save) {
221      try {
222        toProperty(property);
223      } catch (Throwable e) {
224        propertyTable.deactivateEditor(false);
225        propertyTable.handleException(e);
226      }
227    }
228    // dispose Text widget
229    if (m_textControl != null) {
230      m_textControl.dispose();
231      m_textControl = null;
232    }
233  }
234
235  @Override
236  public void keyDown(PropertyTable propertyTable, Property property, KeyEvent event)
237      throws Exception {
238    boolean withAlt = (event.stateMask & SWT.ALT) != 0;
239    boolean withCtrl = (event.stateMask & SWT.CTRL) != 0;
240    if (event.character != 0 && !(withAlt || withCtrl)) {
241      propertyTable.activateEditor(property, null);
242      postKeyEvent(SWT.KeyDown, event);
243      postKeyEvent(SWT.KeyUp, event);
244    }
245  }
246
247  /**
248   * Posts low-level {@link SWT.KeyDown} or {@link SWT.KeyUp} event.
249   */
250  private static void postKeyEvent(int type, KeyEvent event) {
251    Event lowEvent = new Event();
252    lowEvent.type = type;
253    lowEvent.keyCode = event.keyCode;
254    lowEvent.character = event.character;
255    // post event
256    Display.getCurrent().post(lowEvent);
257  }
258
259  ////////////////////////////////////////////////////////////////////////////
260  //
261  // Events
262  //
263  ////////////////////////////////////////////////////////////////////////////
264  /**
265   * Handles {@link KeyListener#keyPressed(KeyEvent)}.
266   */
267  private void handleKeyPressed(PropertyTable propertyTable, Property property, KeyEvent e)
268      throws Exception {
269    if (e.keyCode == SWT.CR) {
270      toProperty(property);
271
272      // BEGIN ADT MODIFICATIONS
273      // After pressing return, dismiss the text cursor to make the value "committed".
274      // I'm not sure why this is necessary here and not in WindowBuilder proper.
275      propertyTable.deactivateEditor(true);
276      // END ADT MODIFICATIONS
277    } else if (e.keyCode == SWT.ESC) {
278      propertyTable.deactivateEditor(false);
279    } else if (e.keyCode == SWT.ARROW_UP || e.keyCode == SWT.ARROW_DOWN) {
280      e.doit = false;
281      boolean success = toProperty(property);
282      // don't allow navigation if current text can not be transferred to property
283      if (!success) {
284        return;
285      }
286      // OK, deactivate and navigate
287      propertyTable.deactivateEditor(true);
288      propertyTable.navigate(e);
289    }
290  }
291
292  ////////////////////////////////////////////////////////////////////////////
293  //
294  // Implementation
295  //
296  ////////////////////////////////////////////////////////////////////////////
297  private String m_currentText;
298
299  /**
300   * Transfers data from {@link Property} to widget.
301   */
302  private void toWidget(Property property) throws Exception {
303    // prepare text
304    String text = getEditorText(property);
305    if (text == null) {
306      text = "";
307    }
308    // set text
309    m_currentText = text;
310    m_textControl.setText(text);
311    m_textControl.selectAll();
312  }
313
314  /**
315   * Transfers data from widget to {@link Property}.
316   *
317   * @return <code>true</code> if transfer was successful.
318   */
319  private boolean toProperty(Property property) throws Exception {
320    // BEGIN ADT MODIFICATIONS
321    if (m_textControl == null) {
322      return false;
323    }
324    // END ADT MODIFICATIONS
325    String text = m_textControl.getText();
326    // change property only if text was changed
327    if (!m_currentText.equals(text)) {
328      m_ignoreFocusLost = true;
329      try {
330        boolean success = setEditorText(property, text);
331        if (!success) {
332          return false;
333        }
334      } finally {
335        m_ignoreFocusLost = false;
336      }
337      // if value was successfully changed, update current text
338      m_currentText = text;
339    }
340    // OK, success
341    return true;
342  }
343
344  ////////////////////////////////////////////////////////////////////////////
345  //
346  // Operations
347  //
348  ////////////////////////////////////////////////////////////////////////////
349  /**
350   * @return <code>true</code> if this editor can modify text.
351   */
352  protected boolean isEditable() {
353    return true;
354  }
355
356  /**
357   * @return the text to display in {@link Text} control.
358   */
359  protected abstract String getEditorText(Property property) throws Exception;
360
361  /**
362   * Modifies {@link Property} using given text.
363   *
364   * @return <code>true</code> if {@link Property} was successfully modified.
365   */
366  protected abstract boolean setEditorText(Property property, String text) throws Exception;
367}
368