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.core.controls;
12
13import org.eclipse.wb.draw2d.IColorConstants;
14import org.eclipse.wb.internal.core.model.property.table.PropertyTable;
15import org.eclipse.wb.internal.core.utils.binding.editors.controls.DefaultControlActionsManager;
16
17import org.eclipse.swt.SWT;
18import org.eclipse.swt.events.SelectionListener;
19import org.eclipse.swt.graphics.Image;
20import org.eclipse.swt.graphics.Point;
21import org.eclipse.swt.graphics.Rectangle;
22import org.eclipse.swt.layout.FillLayout;
23import org.eclipse.swt.widgets.Button;
24import org.eclipse.swt.widgets.Composite;
25import org.eclipse.swt.widgets.Event;
26import org.eclipse.swt.widgets.Listener;
27import org.eclipse.swt.widgets.Shell;
28import org.eclipse.swt.widgets.Table;
29import org.eclipse.swt.widgets.TableColumn;
30import org.eclipse.swt.widgets.TableItem;
31import org.eclipse.swt.widgets.TypedListener;
32import org.eclipse.swt.widgets.Widget;
33
34/**
35 * Combo control for {@link PropertyTable} and combo property editors.
36 *
37 * @author scheglov_ke
38 * @coverage core.control
39 */
40public class CCombo3 extends Composite {
41  private final long m_createTime = System.currentTimeMillis();
42  private final CImageLabel m_text;
43  private final Button m_arrow;
44  private final Shell m_popup;
45  private final Table m_table;
46  private boolean m_fullDropdownTableSize = false;
47
48  ////////////////////////////////////////////////////////////////////////////
49  //
50  // Constructor
51  //
52  ////////////////////////////////////////////////////////////////////////////
53  public CCombo3(Composite parent, int style) {
54    super(parent, style);
55    addEvents(this, m_comboListener, new int[]{SWT.Dispose, SWT.Move, SWT.Resize});
56    // create label
57    {
58      m_text = new CImageLabel(this, SWT.NONE);
59      new DefaultControlActionsManager(m_text);
60      addEvents(m_text, m_textListener, new int[]{
61          SWT.KeyDown,
62          SWT.KeyUp,
63          SWT.MouseDown,
64          SWT.MouseUp,
65          SWT.MouseMove,
66          SWT.MouseDoubleClick,
67          SWT.Traverse,
68          SWT.FocusIn,
69          SWT.FocusOut});
70    }
71    // create arrow
72    {
73      m_arrow = new Button(this, SWT.ARROW | SWT.DOWN);
74      addEvents(m_arrow, m_arrowListener, new int[]{SWT.Selection, SWT.FocusIn, SWT.FocusOut});
75    }
76    // create popup Shell
77    {
78      Shell shell = getShell();
79      m_popup = new Shell(shell, SWT.NONE);
80      m_popup.setLayout(new FillLayout());
81    }
82    // create table for items
83    {
84      m_table = new Table(m_popup, SWT.FULL_SELECTION);
85      addEvents(m_table, m_tableListener, new int[]{SWT.Selection, SWT.FocusIn, SWT.FocusOut});
86      //
87      new TableColumn(m_table, SWT.NONE);
88    }
89    // Focus tracking filter
90    {
91      final Listener filter = new Listener() {
92        private boolean hasFocus;
93
94        public void handleEvent(Event event) {
95          boolean old_hasFocus = hasFocus;
96          hasFocus =
97              m_text.isFocusControl()
98                  || m_arrow.isFocusControl()
99                  || m_popup.isFocusControl()
100                  || m_table.isFocusControl();
101          // configure colors
102          if (hasFocus) {
103            m_text.setBackground(IColorConstants.listSelection);
104            m_text.setForeground(IColorConstants.listSelectionText);
105          } else {
106            m_text.setBackground(IColorConstants.listBackground);
107            m_text.setForeground(IColorConstants.listForeground);
108          }
109          // send FocusOut event
110          if (old_hasFocus && !hasFocus) {
111            Event e = new Event();
112            e.widget = CCombo3.this;
113            e.time = event.time;
114            notifyListeners(SWT.FocusOut, e);
115          }
116        }
117      };
118      getDisplay().addFilter(SWT.FocusIn, filter);
119      addListener(SWT.Dispose, new Listener() {
120        public void handleEvent(Event event) {
121          getDisplay().removeFilter(SWT.FocusIn, filter);
122        }
123      });
124    }
125  }
126
127  ////////////////////////////////////////////////////////////////////////////
128  //
129  // Events handling
130  //
131  ////////////////////////////////////////////////////////////////////////////
132  private final Listener m_comboListener = new Listener() {
133    public void handleEvent(Event event) {
134      switch (event.type) {
135        case SWT.Dispose :
136          if (!m_popup.isDisposed()) {
137            m_popup.dispose();
138          }
139          break;
140        case SWT.Move :
141          doDropDown(false);
142          break;
143        case SWT.Resize :
144          doResize();
145          break;
146      }
147    }
148  };
149  private final Listener m_textListener = new Listener() {
150    public void handleEvent(final Event event) {
151      switch (event.type) {
152        case SWT.MouseDown :
153          if (System.currentTimeMillis() - m_createTime < 400) {
154            // send "logical" double click for case when we just activated combo
155            // and almost right away click second time (but first time on editor)
156            event.detail = -1;
157            notifyListeners(SWT.MouseDoubleClick, event);
158            // when we use "auto drop on editor activation" option, this click is
159            // is "logically" second one, so it should close combo
160            if (!isDisposed()) {
161              doDropDown(false);
162            }
163          } else {
164            m_text.setCapture(true);
165            doDropDown(!isDropped());
166          }
167          break;
168        case SWT.MouseUp : {
169          m_text.setCapture(false);
170          TableItem item = getItemUnderCursor(event);
171          if (item != null) {
172            doDropDown(false);
173            sendSelectionEvent(event);
174          }
175          break;
176        }
177        case SWT.MouseDoubleClick :
178          // prevent resending MouseDoubleClick that we sent on fast MouseDown
179          if (event.detail != -1) {
180            notifyListeners(SWT.MouseDoubleClick, event);
181          }
182          break;
183        case SWT.MouseMove : {
184          TableItem item = getItemUnderCursor(event);
185          if (item != null) {
186            m_table.setSelection(new TableItem[]{item});
187          }
188          break;
189        }
190        case SWT.KeyDown : {
191          // check for keyboard navigation and selection
192          {
193            int selectionIndex = m_table.getSelectionIndex();
194            if (event.keyCode == SWT.ARROW_UP) {
195              selectionIndex--;
196              if (selectionIndex < 0) {
197                selectionIndex = m_table.getItemCount() - 1;
198              }
199              m_table.setSelection(selectionIndex);
200              return;
201            } else if (event.keyCode == SWT.ARROW_DOWN) {
202              m_table.setSelection((selectionIndex + 1) % m_table.getItemCount());
203              return;
204            } else if (event.character == SWT.CR || event.character == ' ') {
205              sendSelectionEvent(event);
206              return;
207            }
208          }
209          // be default just resend event
210          resendKeyEvent(event);
211          break;
212        }
213        case SWT.KeyUp :
214          resendKeyEvent(event);
215          break;
216      }
217    }
218
219    private TableItem getItemUnderCursor(Event event) {
220      Point displayLocation = m_text.toDisplay(new Point(event.x, event.y));
221      Point tableLocation = m_table.toControl(displayLocation);
222      return m_table.getItem(tableLocation);
223    }
224  };
225  private final Listener m_arrowListener = new Listener() {
226    public void handleEvent(Event event) {
227      switch (event.type) {
228      /*case SWT.FocusIn : {
229       resendFocusEvent(event);
230       break;
231       }*/
232        case SWT.Selection : {
233          doDropDown(!isDropped());
234          break;
235        }
236      }
237    }
238  };
239  private final Listener m_tableListener = new Listener() {
240    public void handleEvent(Event event) {
241      switch (event.type) {
242        case SWT.Selection : {
243          doDropDown(false);
244          // show selected item in text
245          {
246            int index = m_table.getSelectionIndex();
247            select(index);
248          }
249          // send selection event
250          sendSelectionEvent(event);
251          break;
252        }
253      }
254    }
255  };
256
257  ////////////////////////////////////////////////////////////////////////////
258  //
259  // Events utils
260  //
261  ////////////////////////////////////////////////////////////////////////////
262  /**
263   * Sends selection event.
264   */
265  private void sendSelectionEvent(Event event) {
266    Event e = new Event();
267    e.time = event.time;
268    e.stateMask = event.stateMask;
269    notifyListeners(SWT.Selection, e);
270  }
271
272  /**
273   * Resends KeyDown/KeyUp events.
274   */
275  private void resendKeyEvent(Event event) {
276    Event e = new Event();
277    e.time = event.time;
278    e.character = event.character;
279    e.keyCode = event.keyCode;
280    e.stateMask = event.stateMask;
281    notifyListeners(event.type, e);
282  }
283
284  /**
285   * Adds given listener as handler for events in given widget.
286   */
287  private void addEvents(Widget widget, Listener listener, int[] events) {
288    for (int i = 0; i < events.length; i++) {
289      widget.addListener(events[i], listener);
290    }
291  }
292
293  /**
294   * Adds the listener to receive events.
295   */
296  public void addSelectionListener(SelectionListener listener) {
297    checkWidget();
298    if (listener == null) {
299      SWT.error(SWT.ERROR_NULL_ARGUMENT);
300    }
301    TypedListener typedListener = new TypedListener(listener);
302    addListener(SWT.Selection, typedListener);
303    addListener(SWT.DefaultSelection, typedListener);
304  }
305
306  ////////////////////////////////////////////////////////////////////////////
307  //
308  // Activity
309  //
310  ////////////////////////////////////////////////////////////////////////////
311  /**
312   * Sets drop state of combo.
313   */
314  public void doDropDown(boolean drop) {
315    // check, may be we already in this drop state
316    if (drop == isDropped()) {
317      return;
318    }
319    // close combo
320    if (!drop) {
321      m_popup.setVisible(false);
322      m_text.setFocus();
323      return;
324    }
325    // open combo
326    {
327      // prepare popup location
328      Point comboSize = getSize();
329      Point popupLocation;
330      {
331        //popupLocation = getParent().toDisplay(getLocation());
332        popupLocation = toDisplay(new Point(0, 0));
333        popupLocation.y += comboSize.y;
334      }
335      // calculate and set popup location
336      {
337        TableColumn tableColumn = m_table.getColumn(0);
338        // pack everything
339        tableColumn.pack();
340        m_table.pack();
341        m_popup.pack();
342        // calculate bounds
343        Rectangle tableBounds = m_table.getBounds();
344        tableBounds.height = Math.min(tableBounds.height, m_table.getItemHeight() * 20); // max 20 items without scrolling
345        m_table.setBounds(tableBounds);
346        // calculate size
347        int remainingDisplayHeight = getDisplay().getClientArea().height - popupLocation.y - 10;
348        int preferredHeight = Math.min(tableBounds.height, remainingDisplayHeight);
349        int remainingDisplayWidth = getDisplay().getClientArea().width - popupLocation.x - 5;
350        int preferredWidth =
351            isFullDropdownTableWidth()
352                ? Math.min(tableBounds.width, remainingDisplayWidth)
353                : comboSize.x;
354        // set popup bounds calculated as computeTrim basing on combo width and table height paying attention on remaining display space
355        Rectangle popupBounds =
356            m_popup.computeTrim(popupLocation.x, popupLocation.y, preferredWidth, preferredHeight);
357        m_popup.setBounds(popupBounds);
358        // adjust column size
359        tableColumn.setWidth(m_table.getClientArea().width);
360      }
361      m_popup.setVisible(true);
362      // scroll to selection if needed
363      m_table.showSelection();
364    }
365  }
366
367  /**
368   * Initiates "press-hold-drag" sequence.
369   */
370  public void startDrag() {
371    m_text.setCapture(true);
372  }
373
374  ////////////////////////////////////////////////////////////////////////////
375  //
376  // Access
377  //
378  ////////////////////////////////////////////////////////////////////////////
379  public void setFullDropdownTableWidth(boolean freeTableSize) {
380    m_fullDropdownTableSize = freeTableSize;
381  }
382
383  public boolean isFullDropdownTableWidth() {
384    return m_fullDropdownTableSize;
385  }
386
387  public boolean isDropped() {
388    return m_popup.isVisible();
389  }
390
391  public void setQuickSearch(boolean value) {
392    // TODO
393  }
394
395  ////////////////////////////////////////////////////////////////////////////
396  //
397  // Access: items
398  //
399  ////////////////////////////////////////////////////////////////////////////
400  /**
401   * Removes all items.
402   */
403  public void removeAll() {
404    TableItem[] items = m_table.getItems();
405    for (int index = 0; index < items.length; index++) {
406      TableItem item = items[index];
407      item.dispose();
408    }
409  }
410
411  /**
412   * Adds new item with given text.
413   */
414  public void add(String text) {
415    add(text, null);
416  }
417
418  /**
419   * Adds new item with given text and image.
420   */
421  public void add(String text, Image image) {
422    checkWidget();
423    TableItem item = new TableItem(m_table, SWT.NONE);
424    item.setText(text);
425    item.setImage(image);
426  }
427
428  /**
429   * @return an item at given index
430   */
431  public String getItem(int index) {
432    checkWidget();
433    return m_table.getItem(index).getText();
434  }
435
436  /**
437   * @return the number of items
438   */
439  public int getItemCount() {
440    checkWidget();
441    return m_table.getItemCount();
442  }
443
444  /**
445   * @return the index of the selected item
446   */
447  public int getSelectionIndex() {
448    checkWidget();
449    return m_table.getSelectionIndex();
450  }
451
452  /**
453   * Selects an item with given index.
454   */
455  public void select(int index) {
456    checkWidget();
457    if (index == -1) {
458      m_table.deselectAll();
459      m_text.setText(null);
460      m_text.setImage(null);
461      return;
462    } else {
463      TableItem item = m_table.getItem(index);
464      m_text.setText(item.getText());
465      m_text.setImage(item.getImage());
466      m_table.select(index);
467    }
468  }
469
470  ////////////////////////////////////////////////////////////////////////////
471  //
472  // Access: text and image
473  //
474  ////////////////////////////////////////////////////////////////////////////
475  /**
476   * Selects item with given text.
477   */
478  public void setText(String text) {
479    // try to find item with given text
480    TableItem[] items = m_table.getItems();
481    for (int index = 0; index < items.length; index++) {
482      TableItem item = items[index];
483      if (item.getText().equals(text)) {
484        select(index);
485        return;
486      }
487    }
488    // not found, remove selection
489    select(-1);
490  }
491
492  ////////////////////////////////////////////////////////////////////////////
493  //
494  // Resize support
495  // TODO: computeSize
496  //
497  ////////////////////////////////////////////////////////////////////////////
498  protected void doResize() {
499    Rectangle clientArea = getClientArea();
500    int areaWidth = clientArea.width;
501    int areaHeight = clientArea.height;
502    // compute sizes of controls
503    Point buttonSize = m_arrow.computeSize(areaHeight, areaHeight);
504    Point textSize = m_text.computeSize(areaWidth - buttonSize.x, areaHeight);
505    // set controls location/size
506    m_arrow.setLocation(areaWidth - buttonSize.x, 0);
507    m_arrow.setSize(buttonSize);
508    m_text.setSize(areaWidth - buttonSize.x, Math.max(textSize.y, areaHeight));
509  }
510}
511