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 com.google.common.collect.Lists;
14
15import org.eclipse.jface.viewers.IBaseLabelProvider;
16import org.eclipse.jface.viewers.IContentProvider;
17import org.eclipse.jface.viewers.IStructuredContentProvider;
18import org.eclipse.jface.viewers.LabelProvider;
19import org.eclipse.jface.viewers.TableViewer;
20import org.eclipse.jface.viewers.TableViewerColumn;
21import org.eclipse.jface.viewers.Viewer;
22import org.eclipse.jface.viewers.ViewerFilter;
23import org.eclipse.swt.SWT;
24import org.eclipse.swt.events.ControlAdapter;
25import org.eclipse.swt.events.ControlEvent;
26import org.eclipse.swt.events.DisposeEvent;
27import org.eclipse.swt.events.DisposeListener;
28import org.eclipse.swt.events.KeyAdapter;
29import org.eclipse.swt.events.KeyEvent;
30import org.eclipse.swt.events.ModifyEvent;
31import org.eclipse.swt.events.ModifyListener;
32import org.eclipse.swt.events.PaintEvent;
33import org.eclipse.swt.events.PaintListener;
34import org.eclipse.swt.events.SelectionAdapter;
35import org.eclipse.swt.events.SelectionEvent;
36import org.eclipse.swt.events.SelectionListener;
37import org.eclipse.swt.events.TypedEvent;
38import org.eclipse.swt.graphics.Image;
39import org.eclipse.swt.graphics.Point;
40import org.eclipse.swt.graphics.Rectangle;
41import org.eclipse.swt.layout.FillLayout;
42import org.eclipse.swt.widgets.Button;
43import org.eclipse.swt.widgets.Canvas;
44import org.eclipse.swt.widgets.Composite;
45import org.eclipse.swt.widgets.Display;
46import org.eclipse.swt.widgets.Event;
47import org.eclipse.swt.widgets.Listener;
48import org.eclipse.swt.widgets.Shell;
49import org.eclipse.swt.widgets.Table;
50import org.eclipse.swt.widgets.TableColumn;
51import org.eclipse.swt.widgets.TableItem;
52import org.eclipse.swt.widgets.Text;
53import org.eclipse.swt.widgets.TypedListener;
54import org.eclipse.wb.internal.core.model.property.editor.TextControlActionsManager;
55import org.eclipse.wb.internal.core.model.property.table.PropertyTable;
56import org.eclipse.wb.internal.core.utils.check.Assert;
57
58import java.util.ArrayList;
59
60/**
61 * Extended ComboBox control for {@link PropertyTable} and combo property editors.
62 *
63 * @author sablin_aa
64 * @coverage core.control
65 */
66public class CComboBox extends Composite {
67  private Text m_text;
68  private Button m_button;
69  private Canvas m_canvas;
70  private Shell m_popup;
71  private TableViewer m_table;
72  private boolean m_fullDropdownTableWidth = false;
73  private boolean m_wasFocused;
74
75  ////////////////////////////////////////////////////////////////////////////
76  //
77  // Constructor
78  //
79  ////////////////////////////////////////////////////////////////////////////
80  public CComboBox(Composite parent, int style) {
81    super(parent, style);
82    createContents(this);
83    m_wasFocused = isComboFocused();
84    // add display hook
85    final Listener displayFocusInHook = new Listener() {
86      @Override
87    public void handleEvent(Event event) {
88        boolean focused = isComboFocused();
89        if (m_wasFocused && !focused) {
90          // close DropDown on focus out ComboBox
91          comboDropDown(false);
92        }
93        if (event.widget != CComboBox.this) {
94          // forward to ComboBox listeners
95          if (!m_wasFocused && focused) {
96            event.widget = CComboBox.this;
97            notifyListeners(SWT.FocusIn, event);
98          }
99          if (m_wasFocused && !focused) {
100            event.widget = CComboBox.this;
101            notifyListeners(SWT.FocusOut, event);
102          }
103        }
104        m_wasFocused = focused;
105      }
106    };
107    final Listener displayFocusOutHook = new Listener() {
108      @Override
109    public void handleEvent(Event event) {
110        m_wasFocused = isComboFocused();
111      }
112    };
113    {
114      Display display = getDisplay();
115      display.addFilter(SWT.FocusIn, displayFocusInHook);
116      display.addFilter(SWT.FocusOut, displayFocusOutHook);
117    }
118    // combo listeners
119    addControlListener(new ControlAdapter() {
120      @Override
121      public void controlResized(ControlEvent e) {
122        resizeInner();
123      }
124    });
125    addDisposeListener(new DisposeListener() {
126      @Override
127    public void widgetDisposed(DisposeEvent e) {
128        {
129          // remove Display hooks
130          Display display = getDisplay();
131          display.removeFilter(SWT.FocusIn, displayFocusInHook);
132          display.removeFilter(SWT.FocusOut, displayFocusOutHook);
133        }
134        disposeInner();
135      }
136    });
137  }
138
139  ////////////////////////////////////////////////////////////////////////////
140  //
141  // Contents
142  //
143  ////////////////////////////////////////////////////////////////////////////
144  protected void createContents(Composite parent) {
145    createText(parent);
146    createButton(parent);
147    createImage(parent);
148    createPopup(parent);
149  }
150
151  /**
152   * Create Text widget.
153   */
154  protected void createText(Composite parent) {
155    m_text = new Text(parent, SWT.NONE);
156    new TextControlActionsManager(m_text);
157    // key press processing
158    m_text.addKeyListener(new KeyAdapter() {
159      @Override
160      public void keyPressed(KeyEvent e) {
161        switch (e.keyCode) {
162          case SWT.ESC :
163            if (isDroppedDown()) {
164              // close dropdown
165              comboDropDown(false);
166              e.doit = false;
167            } else {
168              // forward to ComboBox listeners
169              notifyListeners(SWT.KeyDown, convert2event(e));
170            }
171            break;
172          case SWT.ARROW_UP :
173            if (isDroppedDown()) {
174              // prev item in dropdown list
175              Table table = m_table.getTable();
176              int index = table.getSelectionIndex() - 1;
177              table.setSelection(index < 0 ? table.getItemCount() - 1 : index);
178              e.doit = false;
179            } else {
180              // forward to ComboBox listeners
181              notifyListeners(SWT.KeyDown, convert2event(e));
182            }
183            break;
184          case SWT.ARROW_DOWN :
185            if (isDroppedDown()) {
186              // next item in dropdown list
187              Table table = m_table.getTable();
188              int index = table.getSelectionIndex() + 1;
189              table.setSelection(index == table.getItemCount() ? 0 : index);
190              e.doit = false;
191            } else if ((e.stateMask & SWT.ALT) != 0) {
192              // force drop down combo
193              comboDropDown(true);
194              e.doit = false;
195              // return focus to text
196              setFocus2Text(false);
197            } else {
198              // forward to ComboBox listeners
199              notifyListeners(SWT.KeyDown, convert2event(e));
200            }
201            break;
202          case '\r' :
203            Table table = m_table.getTable();
204            if (isDroppedDown() && table.getSelectionIndex() != -1) {
205              // forward to Table listeners
206              table.notifyListeners(SWT.Selection, convert2event(e));
207            } else {
208              m_text.selectAll();
209              setSelectionText(getEditText());
210              // forward to ComboBox listeners
211              notifyListeners(SWT.Selection, convert2event(e));
212            }
213            break;
214        }
215      }
216    });
217    // modifications processing
218    m_text.addModifyListener(new ModifyListener() {
219      @Override
220    public void modifyText(ModifyEvent e) {
221        if (isDroppedDown()) {
222          m_table.refresh();
223        } else {
224          // force drop down combo
225          if (m_text.isFocusControl()) {
226            comboDropDown(true);
227            // return focus to text
228            setFocus2Text(false);
229          }
230        }
231      }
232    });
233  }
234
235  /**
236   * Create arrow button.
237   */
238  protected void createButton(Composite parent) {
239    m_button = new Button(parent, SWT.ARROW | SWT.DOWN);
240    m_button.addSelectionListener(new SelectionAdapter() {
241      @Override
242      public void widgetSelected(SelectionEvent e) {
243        comboDropDown(!isDroppedDown());
244        // return focus to text
245        setFocus2Text(true);
246      }
247    });
248  }
249
250  /**
251   * Create image canvas.
252   */
253  protected void createImage(Composite parent) {
254    m_canvas = new Canvas(parent, SWT.BORDER);
255    m_canvas.addPaintListener(new PaintListener() {
256      @Override
257    public void paintControl(PaintEvent e) {
258        Image selectionImage = getSelectionImage();
259        if (selectionImage != null) {
260          e.gc.drawImage(selectionImage, 0, 0);
261        } else {
262          e.gc.fillRectangle(m_canvas.getClientArea());
263        }
264      }
265    });
266  }
267
268  /**
269   * Create popup shell with table.
270   */
271  protected void createPopup(Composite parent) {
272    m_popup = new Shell(getShell(), SWT.BORDER);
273    m_popup.setLayout(new FillLayout());
274    createTable(m_popup);
275  }
276
277  /**
278   * Create table.
279   */
280  protected void createTable(Composite parent) {
281    m_table = new TableViewer(parent, SWT.FULL_SELECTION);
282    new TableViewerColumn(m_table, SWT.LEFT);
283    m_table.getTable().addSelectionListener(new SelectionAdapter() {
284      @Override
285      public void widgetSelected(SelectionEvent e) {
286        int selectionIndex = m_table.getTable().getSelectionIndex();
287        setSelectionIndex(selectionIndex);
288        comboDropDown(false);
289        // forward to ComboBox listeners
290        notifyListeners(SWT.Selection, convert2event(e));
291      }
292    });
293    m_table.setContentProvider(getContentProvider());
294    m_table.setLabelProvider(getLabelProvider());
295    m_table.addFilter(getFilterProvider());
296  }
297
298  /**
299   * Placement inner widgets.
300   */
301  protected void resizeInner() {
302    Rectangle clientArea = getClientArea();
303    int rightOccupied = 0;
304    int leftOccupied = 0;
305    {
306      // button
307      m_button.setBounds(
308          clientArea.width - clientArea.height,
309          0,
310          clientArea.height,
311          clientArea.height);
312      rightOccupied = clientArea.height;
313    }
314    {
315      Image selectionImage = getSelectionImage();
316      if (selectionImage != null) {
317        // image
318        m_canvas.setSize(clientArea.height, clientArea.height);
319        leftOccupied = clientArea.height;
320      } else {
321        m_canvas.setSize(1, clientArea.height);
322        leftOccupied = 1;
323      }
324    }
325    {
326      // text
327      m_text.setBounds(
328          leftOccupied,
329          0,
330          clientArea.width - rightOccupied - leftOccupied,
331          clientArea.height);
332    }
333  }
334
335  /**
336   * Dispose inner widgets.
337   */
338  protected void disposeInner() {
339    if (!m_popup.isDisposed()) {
340      m_popup.dispose();
341    }
342  }
343
344  ////////////////////////////////////////////////////////////////////////////
345  //
346  // Providers
347  //
348  ////////////////////////////////////////////////////////////////////////////
349  protected IContentProvider getContentProvider() {
350    return new IStructuredContentProvider() {
351      @Override
352    public Object[] getElements(Object inputElement) {
353        return m_items.toArray(new ComboBoxItem[m_items.size()]);
354      }
355
356      @Override
357    public void dispose() {
358      }
359
360      @Override
361    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
362      }
363    };
364  }
365
366  protected IBaseLabelProvider getLabelProvider() {
367    return new LabelProvider() {
368      @Override
369      public Image getImage(Object element) {
370        ComboBoxItem item = (ComboBoxItem) element;
371        return item.m_image;
372      }
373
374      @Override
375      public String getText(Object element) {
376        ComboBoxItem item = (ComboBoxItem) element;
377        return item.m_label;
378      }
379    };
380  }
381
382  protected ViewerFilter getFilterProvider() {
383    return new ViewerFilter() {
384      @Override
385      public boolean select(Viewer viewer, Object parentElement, Object element) {
386        String lookingString = m_text.getText().toLowerCase();
387        if (isDroppedDown() && lookingString.length() > 0) {
388          ComboBoxItem item = (ComboBoxItem) element;
389          return item.m_label.toLowerCase().indexOf(lookingString) != -1;
390        }
391        return true;
392      }
393    };
394  }
395
396  ////////////////////////////////////////////////////////////////////////////
397  //
398  // Items
399  //
400  ////////////////////////////////////////////////////////////////////////////
401  protected static class ComboBoxItem {
402    public final String m_label;
403    public final Image m_image;
404
405    public ComboBoxItem(String label, Image image) {
406      m_label = label;
407      m_image = image;
408    }
409  }
410
411  ArrayList<ComboBoxItem> m_items = Lists.newArrayList();
412
413  /**
414   * Add new item.
415   */
416  public void addItem(String label, Image image) {
417    Assert.isTrue(!isDroppedDown());
418    m_items.add(new ComboBoxItem(label, image));
419  }
420
421  public void addItem(String label) {
422    addItem(label, null);
423  }
424
425  public void removeAll() {
426    m_items.clear();
427  }
428
429  public int getItemCount() {
430    return m_items.size();
431  }
432
433  public String getItemLabel(int index) {
434    return m_items.get(index).m_label;
435  }
436
437  ////////////////////////////////////////////////////////////////////////////
438  //
439  // Access
440  //
441  ////////////////////////////////////////////////////////////////////////////
442  public boolean isComboFocused() {
443    return isFocusControl()
444        || m_text.isFocusControl()
445        || m_button.isFocusControl()
446        || m_canvas.isFocusControl()
447        || m_popup.isFocusControl()
448        || m_table.getTable().isFocusControl();
449  }
450
451  /**
452   * Edit text.
453   */
454  public String getEditText() {
455    return m_text.getText();
456  }
457
458  public void setEditText(String text) {
459    m_text.setText(text == null ? "" : text);
460    m_text.selectAll();
461  }
462
463  public void setEditSelection(int start, int end) {
464    m_text.setSelection(start, end);
465  }
466
467  /**
468   * Read only.
469   */
470  public void setReadOnly(boolean value) {
471    m_text.setEditable(!value);
472    m_button.setEnabled(!value);
473  }
474
475  /**
476   * Drop down width.
477   */
478  public boolean isFullDropdownTableWidth() {
479    return m_fullDropdownTableWidth;
480  }
481
482  public void setFullDropdownTableWidth(boolean value) {
483    Assert.isTrue(!isDroppedDown());
484    m_fullDropdownTableWidth = value;
485  }
486
487  ////////////////////////////////////////////////////////////////////////////
488  //
489  // Selection
490  //
491  ////////////////////////////////////////////////////////////////////////////
492  private int m_selectionIndex = -1;
493
494  /**
495   * Selection index.
496   */
497  public int getSelectionIndex() {
498    return m_selectionIndex;
499  }
500
501  public void setSelectionIndex(int index) {
502    m_selectionIndex = index;
503    if (isDroppedDown()) {
504      m_table.getTable().setSelection(m_selectionIndex);
505    }
506    setEditText(getSelectionText());
507  }
508
509  /**
510   * Selection text.
511   */
512  private String getSelectionText() {
513    if (m_selectionIndex != -1 && isDroppedDown()) {
514      Object itemData = m_table.getTable().getItem(m_selectionIndex).getData();
515      return ((ComboBoxItem) itemData).m_label;
516    }
517    return null;
518  }
519
520  /**
521   * Selection image.
522   */
523  private Image getSelectionImage() {
524    return m_selectionIndex != -1 ? m_items.get(m_selectionIndex).m_image : null;
525  }
526
527  public void setSelectionText(String label) {
528    TableItem[] items = m_table.getTable().getItems();
529    for (int i = 0; i < items.length; i++) {
530      TableItem item = items[i];
531      if (item.getText().equals(label)) {
532        setSelectionIndex(i);
533        return;
534      }
535    }
536    // no such item
537    setSelectionIndex(-1);
538    setEditText(label);
539  }
540
541  /**
542   * Adds the listener to receive events.
543   */
544  public void addSelectionListener(SelectionListener listener) {
545    checkWidget();
546    if (listener == null) {
547      SWT.error(SWT.ERROR_NULL_ARGUMENT);
548    }
549    TypedListener typedListener = new TypedListener(listener);
550    addListener(SWT.Selection, typedListener);
551    addListener(SWT.DefaultSelection, typedListener);
552  }
553
554  ////////////////////////////////////////////////////////////////////////////
555  //
556  // Popup
557  //
558  ////////////////////////////////////////////////////////////////////////////
559  public boolean isDroppedDown() {
560    return m_popup.isVisible();
561  }
562
563  public void comboDropDown(boolean dropdown) {
564    // check, may be we already in this drop state
565    if (dropdown == isDroppedDown()) {
566      return;
567    }
568    // close combo
569    if (dropdown) {
570      // initialize
571      m_table.setInput(m_items);
572      Table table = m_table.getTable();
573      TableColumn column = table.getColumn(0);
574      column.pack();
575      table.pack();
576      m_popup.pack();
577      // compute table size
578      Rectangle tableBounds = table.getBounds();
579      tableBounds.height = Math.min(tableBounds.height, table.getItemHeight() * 15);// max 15 items without scrolling
580      table.setBounds(tableBounds);
581      // prepare popup point
582      Point comboLocation = toDisplay(new Point(0, 0));
583      Point comboSize = getSize();
584      // compute popup size
585      Display display = getDisplay();
586      Rectangle clientArea = display.getClientArea();
587      int remainingDisplayHeight = clientArea.height - comboLocation.y - comboSize.y - 10;
588      int preferredHeight = Math.min(tableBounds.height, remainingDisplayHeight);
589      int remainingDisplayWidth = clientArea.width - comboLocation.x - 10;
590      int preferredWidth =
591          isFullDropdownTableWidth()
592              ? Math.min(tableBounds.width, remainingDisplayWidth)
593              : comboSize.x;
594      Rectangle popupBounds =
595          new Rectangle(comboLocation.x,
596              comboLocation.y + comboSize.y,
597              preferredWidth,
598              preferredHeight);
599      Rectangle trimBounds =
600          m_popup.computeTrim(popupBounds.x, popupBounds.y, popupBounds.width, popupBounds.height);
601      m_popup.setBounds(popupBounds.x, popupBounds.y, 2 * popupBounds.width - trimBounds.width, 2
602          * popupBounds.height
603          - trimBounds.height);
604      // adjust column size
605      column.setWidth(table.getClientArea().width);
606      // show popup
607      m_popup.setVisible(true);
608      table.setSelection(getSelectionIndex());
609    } else {
610      // hide popup
611      m_popup.setVisible(false);
612    }
613  }
614
615  protected final void setFocus2Text(final boolean selectAll) {
616    getDisplay().asyncExec(new Runnable() {
617      final boolean m_selectAll = selectAll;
618
619      @Override
620    public void run() {
621        if (!m_text.isDisposed()) {
622          m_text.setFocus();
623          if (m_selectAll) {
624            m_text.selectAll();
625          }
626        }
627      }
628    });
629  }
630
631  ////////////////////////////////////////////////////////////////////////////
632  //
633  // Utilities
634  //
635  ////////////////////////////////////////////////////////////////////////////
636  protected static Event convert2event(TypedEvent tEvent) {
637    Event event = new Event();
638    event.widget = tEvent.widget;
639    event.display = tEvent.display;
640    event.widget = tEvent.widget;
641    event.time = tEvent.time;
642    event.data = tEvent.data;
643    if (tEvent instanceof KeyEvent) {
644      KeyEvent kEvent = (KeyEvent) tEvent;
645      event.character = kEvent.character;
646      event.keyCode = kEvent.keyCode;
647      event.stateMask = kEvent.stateMask;
648      event.doit = kEvent.doit;
649    }
650    if (tEvent instanceof SelectionEvent) {
651      SelectionEvent sEvent = (SelectionEvent) tEvent;
652      event.item = sEvent.item;
653      event.x = sEvent.x;
654      event.y = sEvent.y;
655      event.width = sEvent.width;
656      event.height = sEvent.height;
657      event.detail = sEvent.detail;
658      event.stateMask = sEvent.stateMask;
659      event.text = sEvent.text;
660      event.doit = sEvent.doit;
661    }
662    return event;
663  }
664}
665