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.swt.SWT;
14import org.eclipse.swt.custom.CCombo;
15import org.eclipse.swt.events.DisposeEvent;
16import org.eclipse.swt.events.DisposeListener;
17import org.eclipse.swt.events.SelectionListener;
18import org.eclipse.swt.graphics.Color;
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.Combo;
25import org.eclipse.swt.widgets.Composite;
26import org.eclipse.swt.widgets.Control;
27import org.eclipse.swt.widgets.Display;
28import org.eclipse.swt.widgets.Event;
29import org.eclipse.swt.widgets.Listener;
30import org.eclipse.swt.widgets.Shell;
31import org.eclipse.swt.widgets.Table;
32import org.eclipse.swt.widgets.TableColumn;
33import org.eclipse.swt.widgets.TableItem;
34import org.eclipse.swt.widgets.TypedListener;
35
36import java.util.Locale;
37
38/**
39 * {@link Control} like {@link Combo} or {@link CCombo} that shows {@link Table} with image/text as
40 * drop-down.
41 *
42 * @author mitin_aa
43 * @author scheglov_ke
44 * @coverage core.control
45 */
46public class CTableCombo extends Composite {
47  protected Button m_arrow;
48  protected CImageLabel m_text;
49  protected Shell m_popup;
50  protected Table m_table;
51  protected boolean hasFocus;
52
53  //
54  public CTableCombo(Composite parent, int style) {
55    super(parent, style = checkStyle(style));
56    init(parent, style);
57  }
58
59  static int checkStyle(int style) {
60    int mask = SWT.BORDER | SWT.READ_ONLY | SWT.FLAT;
61    return style & mask;
62  }
63
64  private void init(Composite parent, int style) {
65    m_arrow = new Button(this, SWT.ARROW | SWT.DOWN | SWT.NO_FOCUS);
66    m_text = new CImageLabel(this, style & ~SWT.BORDER);
67    m_text.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
68    final Shell shell = getShell();
69    m_popup = new Shell(shell, SWT.NONE);
70    m_table = new Table(m_popup, SWT.FULL_SELECTION);
71    new TableColumn(m_table, SWT.NONE);
72    Listener listener = new Listener() {
73      public void handleEvent(Event event) {
74        if (m_popup == event.widget) {
75          handlePopupEvent(event);
76          return;
77        }
78        if (m_text == event.widget) {
79          handleTextEvent(event);
80          return;
81        }
82        if (m_table == event.widget) {
83          handleTableEvent(event);
84          return;
85        }
86        if (m_arrow == event.widget) {
87          handleArrowEvent(event);
88          return;
89        }
90        if (CTableCombo.this == event.widget) {
91          handleComboEvent(event);
92          return;
93        }
94      }
95    };
96    final Listener shellListener = new Listener() {
97      public void handleEvent(Event event) {
98        switch (event.type) {
99          case SWT.Dispose :
100          case SWT.Move :
101          case SWT.Resize :
102            if (!isDisposed()) {
103              dropDown(false);
104            }
105            break;
106        }
107      }
108    };
109    final int[] comboEvents = {SWT.Dispose, SWT.Move, SWT.Resize};
110    for (int i = 0; i < comboEvents.length; i++) {
111      addListener(comboEvents[i], listener);
112      // HACK: hide popup when parent changed
113      shell.addListener(comboEvents[i], shellListener);
114    }
115    addDisposeListener(new DisposeListener() {
116      public void widgetDisposed(DisposeEvent e) {
117        for (int i = 0; i < comboEvents.length; i++) {
118          shell.removeListener(comboEvents[i], shellListener);
119        }
120      }
121    });
122    int[] popupEvents = {SWT.Close, SWT.Paint, SWT.Deactivate};
123    for (int i = 0; i < popupEvents.length; i++) {
124      m_popup.addListener(popupEvents[i], listener);
125    }
126    int[] textEvents =
127        {
128            SWT.KeyDown,
129            SWT.KeyUp,
130            SWT.Modify,
131            SWT.MouseDown,
132            SWT.MouseUp,
133            SWT.MouseDoubleClick,
134            SWT.Traverse,
135            SWT.FocusIn,
136            SWT.FocusOut};
137    for (int i = 0; i < textEvents.length; i++) {
138      m_text.addListener(textEvents[i], listener);
139    }
140    int[] tableEvents =
141        {
142            SWT.MouseUp,
143            SWT.Selection,
144            SWT.Traverse,
145            SWT.KeyDown,
146            SWT.KeyUp,
147            SWT.FocusIn,
148            SWT.FocusOut};
149    for (int i = 0; i < tableEvents.length; i++) {
150      m_table.addListener(tableEvents[i], listener);
151    }
152    int[] arrowEvents = {SWT.Selection, SWT.FocusIn, SWT.FocusOut};
153    for (int i = 0; i < arrowEvents.length; i++) {
154      m_arrow.addListener(arrowEvents[i], listener);
155    }
156  }
157
158  protected void handleTableEvent(Event event) {
159    switch (event.type) {
160      case SWT.FocusIn : {
161        if (hasFocus) {
162          return;
163        }
164        hasFocus = true;
165        Event e = new Event();
166        e.time = event.time;
167        notifyListeners(SWT.FocusIn, e);
168        break;
169      }
170      case SWT.FocusOut : {
171        final int time = event.time;
172        event.display.asyncExec(new Runnable() {
173          public void run() {
174            if (CTableCombo.this.isDisposed()) {
175              return;
176            }
177            Control focusControl = getDisplay().getFocusControl();
178            if (focusControl == m_text || focusControl == m_arrow) {
179              return;
180            }
181            hasFocus = false;
182            Event e = new Event();
183            e.time = time;
184            notifyListeners(SWT.FocusOut, e);
185          }
186        });
187        break;
188      }
189      case SWT.MouseUp : {
190        if (event.button != 1) {
191          return;
192        }
193        dropDown(false);
194        Event e = new Event();
195        e.time = event.time;
196        notifyListeners(SWT.DefaultSelection, e);
197        break;
198      }
199      case SWT.Selection : {
200        int index = m_table.getSelectionIndex();
201        if (index == -1) {
202          return;
203        }
204        TableItem item = m_table.getItem(index);
205        m_text.setText(item.getText());
206        m_text.setImage(item.getImage());
207        //m_text.selectAll();
208        m_table.setSelection(index);
209        Event e = new Event();
210        e.time = event.time;
211        e.stateMask = event.stateMask;
212        e.doit = event.doit;
213        notifyListeners(SWT.Selection, e);
214        event.doit = e.doit;
215        dropDown(false);
216        break;
217      }
218      case SWT.Traverse : {
219        switch (event.detail) {
220          case SWT.TRAVERSE_TAB_NEXT :
221          case SWT.TRAVERSE_RETURN :
222          case SWT.TRAVERSE_ESCAPE :
223          case SWT.TRAVERSE_ARROW_PREVIOUS :
224          case SWT.TRAVERSE_ARROW_NEXT :
225            event.doit = false;
226            break;
227        }
228        Event e = new Event();
229        e.time = event.time;
230        e.detail = event.detail;
231        e.doit = event.doit;
232        e.keyCode = event.keyCode;
233        notifyListeners(SWT.Traverse, e);
234        event.doit = e.doit;
235        break;
236      }
237      case SWT.KeyUp : {
238        Event e = new Event();
239        e.time = event.time;
240        e.character = event.character;
241        e.keyCode = event.keyCode;
242        e.stateMask = event.stateMask;
243        notifyListeners(SWT.KeyUp, e);
244        break;
245      }
246      case SWT.KeyDown : {
247        if (event.character == SWT.ESC) {
248          // escape key cancels popups
249          dropDown(false);
250        }
251        if (event.character == SWT.CR || event.character == '\t') {
252          // Enter and Tab cause default selection
253          dropDown(false);
254          Event e = new Event();
255          e.time = event.time;
256          e.stateMask = event.stateMask;
257          notifyListeners(SWT.DefaultSelection, e);
258        }
259        // At this point the widget may have been disposed.
260        // If so, do not continue.
261        if (isDisposed()) {
262          break;
263        }
264        Event e = new Event();
265        e.time = event.time;
266        e.character = event.character;
267        e.keyCode = event.keyCode;
268        e.stateMask = event.stateMask;
269        notifyListeners(SWT.KeyDown, e);
270        break;
271      }
272    }
273  }
274
275  protected void handlePopupEvent(Event event) {
276    switch (event.type) {
277      case SWT.Paint :
278        // draw black rectangle around list
279        Rectangle listRect = m_table.getBounds();
280        Color black = getDisplay().getSystemColor(SWT.COLOR_BLACK);
281        event.gc.setForeground(black);
282        event.gc.drawRectangle(0, 0, listRect.width + 1, listRect.height + 1);
283        break;
284      case SWT.Close :
285        event.doit = false;
286        dropDown(false);
287        break;
288    }
289  }
290
291  protected void handleComboEvent(Event event) {
292    switch (event.type) {
293      case SWT.Dispose :
294        if (m_popup != null && !m_popup.isDisposed()) {
295          m_popup.dispose();
296        }
297        m_popup = null;
298        m_text = null;
299        m_arrow = null;
300        break;
301      case SWT.Move :
302        dropDown(false);
303        break;
304      case SWT.Resize :
305        internalLayout();
306        break;
307    }
308  }
309
310  protected void handleArrowEvent(Event event) {
311    switch (event.type) {
312      case SWT.FocusIn : {
313        if (hasFocus) {
314          return;
315        }
316        hasFocus = true;
317        Event e = new Event();
318        e.time = event.time;
319        notifyListeners(SWT.FocusIn, e);
320        break;
321      }
322      case SWT.Selection : {
323        boolean wasDropped = isDropped();
324        dropDown(!wasDropped);
325        if (wasDropped) {
326          m_text.forceFocus();
327        }
328        break;
329      }
330    }
331  }
332
333  protected void handleTextEvent(Event event) {
334    switch (event.type) {
335      case SWT.FocusIn : {
336        if (hasFocus) {
337          return;
338        }
339        hasFocus = true;
340        //if (getEditable())
341        Event e = new Event();
342        e.time = event.time;
343        notifyListeners(SWT.FocusIn, e);
344        break;
345      }
346      case SWT.FocusOut : {
347        final int time = event.time;
348        event.display.asyncExec(new Runnable() {
349          public void run() {
350            if (CTableCombo.this.isDisposed()) {
351              return;
352            }
353            Control focusControl = getDisplay().getFocusControl();
354            if (focusControl == m_table
355                || focusControl == m_arrow
356                || focusControl != null
357                && focusControl.getParent() == CTableCombo.this) {
358              return;
359            }
360            hasFocus = false;
361            Event e = new Event();
362            e.time = time;
363            notifyListeners(SWT.FocusOut, e);
364          }
365        });
366        break;
367      }
368      case SWT.KeyDown : {
369        if (event.character == SWT.ESC) { // escape key cancels popup
370          dropDown(false);
371        }
372        if (event.character == SWT.CR) {
373          dropDown(false);
374          Event e = new Event();
375          e.time = event.time;
376          e.stateMask = event.stateMask;
377          notifyListeners(SWT.DefaultSelection, e);
378        }
379        // At this point the widget may have been disposed.
380        // If so, do not continue.
381        if (isDisposed()) {
382          break;
383        }
384        if (event.character == '+') {
385          dropDown(true);
386        }
387        if (isDropped()) {
388          if (event.keyCode == SWT.ARROW_UP || event.keyCode == SWT.ARROW_DOWN) {
389            int oldIndex = getSelectionIndex();
390            if (event.keyCode == SWT.ARROW_UP) {
391              select(Math.max(oldIndex - 1, 0));
392            } else {
393              select(Math.min(oldIndex + 1, getItemCount() - 1));
394            }
395            if (oldIndex != getSelectionIndex()) {
396              Event e = new Event();
397              e.time = event.time;
398              e.stateMask = event.stateMask;
399              notifyListeners(SWT.Selection, e);
400            }
401            // At this point the widget may have been disposed.
402            // If so, do not continue.
403            if (isDisposed()) {
404              break;
405            }
406          }
407        }
408        if (Character.isLetter(event.character)) {
409          int oldIndex = getSelectionIndex();
410          int index = -1;
411          for (int i = 0; i < getItemCount(); i++) {
412            String item = getItem(i).toUpperCase(Locale.ENGLISH);
413            if (item.length() != 0 && item.charAt(0) == Character.toUpperCase(event.character)) {
414              index = i;
415              break;
416            }
417          }
418          if (index != -1) {
419            select(Math.max(index, 0));
420            if (oldIndex != getSelectionIndex()) {
421              Event e = new Event();
422              e.time = event.time;
423              e.stateMask = event.stateMask;
424              notifyListeners(SWT.Selection, e);
425            }
426          }
427        }
428        Event e = new Event();
429        e.time = event.time;
430        e.character = event.character;
431        e.keyCode = event.keyCode;
432        e.stateMask = event.stateMask;
433        if (m_text != null && !m_text.isDisposed()) {
434          notifyListeners(SWT.KeyDown, e);
435        }
436        break;
437      }
438      case SWT.KeyUp : {
439        Event e = new Event();
440        e.time = event.time;
441        e.character = event.character;
442        e.keyCode = event.keyCode;
443        e.stateMask = event.stateMask;
444        notifyListeners(SWT.KeyUp, e);
445        break;
446      }
447      case SWT.Modify : {
448        m_table.deselectAll();
449        Event e = new Event();
450        e.time = event.time;
451        notifyListeners(SWT.Modify, e);
452        break;
453      }
454      case SWT.MouseDown : {
455        if (event.button != 1) {
456          return;
457        }
458        m_text.forceFocus();
459        boolean dropped = isDropped();
460        dropDown(!dropped);
461        if (!dropped) {
462          m_text.forceFocus();
463        }
464        break;
465      }
466      case SWT.MouseDoubleClick : {
467        notifyListeners(SWT.MouseDoubleClick, event);
468        break;
469      }
470      case SWT.Traverse : {
471        switch (event.detail) {
472          case SWT.TRAVERSE_RETURN :
473          case SWT.TRAVERSE_ARROW_PREVIOUS :
474          case SWT.TRAVERSE_ARROW_NEXT :
475            // The enter causes default selection and
476            // the arrow keys are used to manipulate the list contents so
477            // do not use them for traversal.
478            event.doit = false;
479            break;
480          case SWT.TRAVERSE_TAB_NEXT :
481          case SWT.TRAVERSE_TAB_PREVIOUS :
482            event.doit = true;
483            break;
484        }
485        Event e = new Event();
486        e.time = event.time;
487        e.detail = event.detail;
488        e.doit = event.doit;
489        e.keyCode = event.keyCode;
490        notifyListeners(SWT.Traverse, e);
491        event.doit = e.doit;
492        break;
493      }
494    }
495  }
496
497  private void dropDown(boolean drop) {
498    if (drop == isDropped()) {
499      return;
500    }
501    if (!drop) {
502      m_popup.setVisible(false);
503      m_text.setFocus();
504      return;
505    }
506    int index = m_table.getSelectionIndex();
507    if (index != -1) {
508      m_table.setTopIndex(index);
509      m_table.setSelection(index);
510    }
511    m_table.pack();
512    Point point = getParent().toDisplay(getLocation());
513    Point comboSize = getSize();
514    //Rectangle tableRect = m_table.getBounds();
515    //int width = Math.max(comboSize.x, tableRect.width + 2);
516    int width = comboSize.x - 1;
517    // only one column
518    m_table.getColumn(0).setWidth(width - 5);
519    if (!(m_popup.getLayout() instanceof FillLayout)) {
520      m_popup.setLayout(new FillLayout());
521    }
522    int itemCount = m_table.getItemCount();
523    if (itemCount > 20) {
524      itemCount = 20;
525    }
526    int height =
527        Math.min(
528            m_table.getItemHeight() * itemCount + 5,
529            Display.getCurrent().getClientArea().height - point.y - 20);
530    m_popup.setBounds(point.x, point.y + comboSize.y, width, height);
531    m_popup.layout();
532    m_popup.setVisible(true);
533    m_text.setFocus();
534    if (index != -1) {
535      m_table.setTopIndex(index);
536      m_table.setSelection(index);
537    }
538  }
539
540  @Override
541  public Point computeSize(int wHint, int hHint, boolean changed) {
542    checkWidget();
543    int width = 0, height = 0;
544    Point textSize = m_text.computeSize(wHint, SWT.DEFAULT, changed);
545    Point arrowSize = m_arrow.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed);
546    int tableWidth;
547    {
548      TableColumn column = m_table.getColumn(0);
549      column.pack();
550      tableWidth = column.getWidth();
551    }
552    //
553    int borderWidth = getBorderWidth();
554    height = Math.max(hHint, Math.max(textSize.y, arrowSize.y) + 2 * borderWidth);
555    width = Math.max(wHint, Math.max(textSize.x + arrowSize.x, tableWidth) + 2 * borderWidth);
556    //
557    return new Point(width, height);
558  }
559
560  private void internalLayout() {
561    if (isDropped()) {
562      dropDown(false);
563    }
564    Rectangle rect = getClientArea();
565    int width = rect.width;
566    int height = rect.height;
567    Point arrowSize = m_arrow.computeSize(SWT.DEFAULT, height);
568    m_text.setBounds(rect.x, rect.y, width - arrowSize.x, height);
569    m_arrow.setBounds(rect.x + width - arrowSize.x, rect.y, arrowSize.x, arrowSize.y);
570  }
571
572  private boolean isDropped() {
573    return m_popup.isVisible();
574  }
575
576  @Override
577  public boolean isFocusControl() {
578    checkWidget();
579    if (m_text.isFocusControl()
580        || m_arrow.isFocusControl()
581        || m_table.isFocusControl()
582        || m_popup.isFocusControl()) {
583      return true;
584    }
585    return super.isFocusControl();
586  }
587
588  public void select(int index) {
589    checkWidget();
590    if (index == -1) {
591      m_table.deselectAll();
592      m_text.setText(""); //$NON-NLS-1$
593      m_text.setImage(null);
594      return;
595    }
596    if (0 <= index && index < m_table.getItemCount()) {
597      if (index != getSelectionIndex()) {
598        TableItem item = m_table.getItem(index);
599        m_text.setText(item.getText());
600        m_text.setImage(item.getImage());
601        m_table.select(index);
602        m_table.showSelection();
603      }
604    }
605  }
606
607  @Override
608  public void setEnabled(boolean enabled) {
609    super.setEnabled(enabled);
610    if (enabled) {
611      m_text.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
612    } else {
613      m_text.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW));
614    }
615  }
616
617  public String getItem(int index) {
618    checkWidget();
619    return m_table.getItem(index).getText();
620  }
621
622  public int getSelectionIndex() {
623    checkWidget();
624    return m_table.getSelectionIndex();
625  }
626
627  public void removeAll() {
628    checkWidget();
629    m_text.setText(""); //$NON-NLS-1$
630    m_text.setImage(null);
631    m_table.removeAll();
632  }
633
634  public int indexOf(String string) {
635    return indexOf(string, 0);
636  }
637
638  public int indexOf(String string, int start) {
639    checkWidget();
640    if (string == null) {
641      return -1;
642    }
643    TableItem[] items = m_table.getItems();
644    for (int i = start; i < items.length; i++) {
645      TableItem item = items[i];
646      if (item.getText().equalsIgnoreCase(string)) {
647        return i;
648      }
649    }
650    return -1;
651  }
652
653  public String getText() {
654    return m_text.getText();
655  }
656
657  public int getItemCount() {
658    checkWidget();
659    return m_table.getItemCount();
660  }
661
662  protected void setText(String string) {
663    m_text.setText(string);
664  }
665
666  protected void setImage(Image image) {
667    m_text.setImage(image);
668  }
669
670  public void add(String text) {
671    add(text, null);
672  }
673
674  public void add(String text, Image image) {
675    checkWidget();
676    TableItem item = new TableItem(m_table, SWT.NONE);
677    item.setText(text);
678    item.setImage(image);
679  }
680
681  public void addSelectionListener(SelectionListener listener) {
682    checkWidget();
683    if (listener == null) {
684      SWT.error(SWT.ERROR_NULL_ARGUMENT);
685    }
686    TypedListener typedListener = new TypedListener(listener);
687    addListener(SWT.Selection, typedListener);
688    addListener(SWT.DefaultSelection, typedListener);
689  }
690}
691