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.flyout;
12
13import org.eclipse.jface.action.Action;
14import org.eclipse.jface.action.IMenuListener;
15import org.eclipse.jface.action.IMenuManager;
16import org.eclipse.jface.action.MenuManager;
17import org.eclipse.jface.resource.JFaceResources;
18import org.eclipse.swt.SWT;
19import org.eclipse.swt.events.DisposeEvent;
20import org.eclipse.swt.events.DisposeListener;
21import org.eclipse.swt.events.MouseAdapter;
22import org.eclipse.swt.events.MouseEvent;
23import org.eclipse.swt.events.MouseMoveListener;
24import org.eclipse.swt.events.MouseTrackAdapter;
25import org.eclipse.swt.graphics.Font;
26import org.eclipse.swt.graphics.GC;
27import org.eclipse.swt.graphics.Image;
28import org.eclipse.swt.graphics.Point;
29import org.eclipse.swt.graphics.Rectangle;
30import org.eclipse.swt.widgets.Composite;
31import org.eclipse.swt.widgets.Control;
32import org.eclipse.swt.widgets.Event;
33import org.eclipse.swt.widgets.Listener;
34import org.eclipse.swt.widgets.Tracker;
35import org.eclipse.wb.core.controls.Messages;
36import org.eclipse.wb.draw2d.IColorConstants;
37import org.eclipse.wb.draw2d.ICursorConstants;
38import org.eclipse.wb.internal.core.utils.ui.DrawUtils;
39
40import java.util.ArrayList;
41import java.util.List;
42
43/**
44 * {@link FlyoutControlComposite} is container for two {@link Control}'s. One (client control) is
45 * used to fill client area. Second (flyout control) can be docked to any enabled position or
46 * temporary hidden.
47 *
48 * @author scheglov_ke
49 * @coverage core.control
50 */
51public final class FlyoutControlComposite extends Composite {
52  private static final int RESIZE_WIDTH = 5;
53  private static final int TITLE_LINES = 30;
54  private static final int TITLE_MARGIN = 5;
55  private static final Font TITLE_FONT = JFaceResources.getFontRegistry().getBold(
56      JFaceResources.DEFAULT_FONT);
57  ////////////////////////////////////////////////////////////////////////////
58  //
59  // Images
60  //
61  ////////////////////////////////////////////////////////////////////////////
62  private static final Image PIN = loadImage("icons/pin.gif");
63  private static final Image ARROW_LEFT = loadImage("icons/arrow_left.gif");
64  private static final Image ARROW_RIGHT = loadImage("icons/arrow_right.gif");
65  private static final Image ARROW_TOP = loadImage("icons/arrow_top.gif");
66  private static final Image ARROW_BOTTOM = loadImage("icons/arrow_bottom.gif");
67
68  private static Image loadImage(String path) {
69    return DrawUtils.loadImage(FlyoutControlComposite.class, path);
70  }
71
72  ////////////////////////////////////////////////////////////////////////////
73  //
74  // Instance fields
75  //
76  ////////////////////////////////////////////////////////////////////////////
77  private final IFlyoutPreferences m_preferences;
78  private final FlyoutContainer m_flyoutContainer;
79  private int m_minWidth = 150;
80  private int m_validDockLocations = -1;
81  private final List<IFlyoutMenuContributor> m_menuContributors =
82      new ArrayList<IFlyoutMenuContributor>();
83
84  ////////////////////////////////////////////////////////////////////////////
85  //
86  // Constructor
87  //
88  ////////////////////////////////////////////////////////////////////////////
89  public FlyoutControlComposite(Composite parent, int style, IFlyoutPreferences preferences) {
90    super(parent, style);
91    m_preferences = preferences;
92    // add listeners
93    addListener(SWT.Resize, new Listener() {
94      @Override
95    public void handleEvent(Event event) {
96        if (getShell().getMinimized()) {
97          return;
98        }
99        layout();
100      }
101    });
102    // create container for flyout control
103    m_flyoutContainer = new FlyoutContainer(this, SWT.NO_BACKGROUND);
104  }
105
106  ////////////////////////////////////////////////////////////////////////////
107  //
108  // Parents
109  //
110  ////////////////////////////////////////////////////////////////////////////
111  /**
112   * @return the parent {@link Composite} for flyout {@link Control}.
113   */
114  public Composite getFlyoutParent() {
115    return m_flyoutContainer;
116  }
117
118  /**
119   * @return the parent {@link Composite} for client {@link Control}.
120   */
121  public Composite getClientParent() {
122    return this;
123  }
124
125  /**
126   * Sets the bit set with valid docking locations.
127   */
128  public void setValidDockLocations(int validDockLocations) {
129    m_validDockLocations = validDockLocations;
130  }
131
132  ////////////////////////////////////////////////////////////////////////////
133  //
134  // Access
135  //
136  ////////////////////////////////////////////////////////////////////////////
137  /**
138   * Sets the minimal width of flyout.
139   */
140  public void setMinWidth(int minWidth) {
141    m_minWidth = minWidth;
142  }
143
144  /**
145   * Sets the text of title.
146   */
147  public void setTitleText(String text) {
148    m_flyoutContainer.setTitleText(text);
149  }
150
151  /**
152   * Adds new {@link IFlyoutMenuContributor}.
153   */
154  public void addMenuContributor(IFlyoutMenuContributor contributor) {
155    if (!m_menuContributors.contains(contributor)) {
156      m_menuContributors.add(contributor);
157    }
158  }
159
160  ////////////////////////////////////////////////////////////////////////////
161  //
162  // Layout
163  //
164  ////////////////////////////////////////////////////////////////////////////
165  @Override
166  public void layout() {
167    Rectangle clientArea = getClientArea();
168    int state = m_preferences.getState();
169    Control client = getChildren()[1];
170    // check, may be "clientArea" is empty, for example because CTabFolder page is not visible
171    if (clientArea.width == 0 || clientArea.height == 0) {
172      return;
173    }
174    // check, maybe flyout has no Control, so "client" should fill client area
175    if (m_flyoutContainer.getControl() == null
176            // BEGIN ADT MODIFICATIONS
177            || !m_flyoutContainer.getControl().getVisible()
178            // END ADT MODIFICATIONS
179            ) {
180      m_flyoutContainer.setBounds(0, 0, 0, 0);
181      client.setBounds(clientArea);
182      return;
183    }
184    // prepare width to display
185    int width;
186    int offset;
187    if (state == IFlyoutPreferences.STATE_OPEN) {
188      width = m_preferences.getWidth();
189      // limit maximum value
190      if (isHorizontal()) {
191        width = Math.min(clientArea.width / 2, width);
192      } else {
193        width = Math.min(clientArea.height / 2, width);
194      }
195      // limit minimum value
196      width = Math.max(width, m_minWidth);
197      width = Math.max(width, 2 * m_flyoutContainer.m_titleHeight + m_flyoutContainer.m_titleWidth);
198      // remember actual width
199      m_preferences.setWidth(width);
200      //
201      offset = width;
202    } else if (state == IFlyoutPreferences.STATE_EXPANDED) {
203      offset = m_flyoutContainer.m_titleHeight;
204      width = m_preferences.getWidth();
205    } else {
206      width = m_flyoutContainer.m_titleHeight;
207      offset = width;
208    }
209    // change bounds for flyout container and client control
210    {
211      if (isWest()) {
212        m_flyoutContainer.setBounds(0, 0, width, clientArea.height);
213        client.setBounds(offset, 0, clientArea.width - offset, clientArea.height);
214      } else if (isEast()) {
215        m_flyoutContainer.setBounds(clientArea.width - width, 0, width, clientArea.height);
216        client.setBounds(0, 0, clientArea.width - offset, clientArea.height);
217      } else if (isNorth()) {
218        m_flyoutContainer.setBounds(0, 0, clientArea.width, width);
219        client.setBounds(0, offset, clientArea.width, clientArea.height - offset);
220      } else if (isSouth()) {
221        m_flyoutContainer.setBounds(0, clientArea.height - width, clientArea.width, width);
222        client.setBounds(0, 0, clientArea.width, clientArea.height - offset);
223      }
224    }
225  }
226
227  ////////////////////////////////////////////////////////////////////////////
228  //
229  // Internal utils
230  //
231  ////////////////////////////////////////////////////////////////////////////
232  private boolean isHorizontal() {
233    return isWest() || isEast();
234  }
235
236  private boolean isWest() {
237    return getDockLocation() == IFlyoutPreferences.DOCK_WEST;
238  }
239
240  private boolean isEast() {
241    return getDockLocation() == IFlyoutPreferences.DOCK_EAST;
242  }
243
244  private boolean isNorth() {
245    return getDockLocation() == IFlyoutPreferences.DOCK_NORTH;
246  }
247
248  private boolean isSouth() {
249    return getDockLocation() == IFlyoutPreferences.DOCK_SOUTH;
250  }
251
252  /**
253   * @return <code>true</code> if given docking location is valid.
254   */
255  private boolean isValidDockLocation(int location) {
256    return (location & m_validDockLocations) == location;
257  }
258
259  /**
260   * @return current docking location.
261   */
262  private int getDockLocation() {
263    return m_preferences.getDockLocation();
264  }
265
266  /**
267   * Sets new docking location.
268   */
269  private void setDockLocation(int dockLocation) {
270    m_preferences.setDockLocation(dockLocation);
271    layout();
272  }
273
274  // BEGIN ADT MODIFICATIONS
275  /**
276   * Applies the given preferences into the preferences of this flyout
277   * control. This does not cause any visual updates; call {@link #layout()}
278   * to update the widget.
279   *
280   * @param preferences the preferences to apply
281   */
282  public void apply(IFlyoutPreferences preferences) {
283    m_preferences.setDockLocation(preferences.getDockLocation());
284    m_preferences.setState(preferences.getState());
285    m_preferences.setWidth(preferences.getWidth());
286  }
287
288  /** If the flyout hover is showing, dismiss it */
289  public void dismissHover() {
290    if (m_flyoutContainer != null) {
291      m_flyoutContainer.dismissHover();
292    }
293  }
294
295  /** Sets a listener to be modified when windows are opened, collapsed and expanded */
296  public void setListener(IFlyoutListener listener) {
297    assert m_listener == null; // Only one listener supported
298    m_listener = listener;
299  }
300  private IFlyoutListener m_listener;
301  // END ADT MODIFICATIONS
302
303  ////////////////////////////////////////////////////////////////////////////
304  //
305  // FlyoutContainer
306  //
307  ////////////////////////////////////////////////////////////////////////////
308  /**
309   * Container for flyout {@link Control}.
310   *
311   * @author scheglov_ke
312   */
313  private final class FlyoutContainer extends Composite {
314    ////////////////////////////////////////////////////////////////////////////
315    //
316    // Container
317    //
318    ////////////////////////////////////////////////////////////////////////////
319    public FlyoutContainer(Composite parent, int style) {
320      super(parent, style);
321      configureMenu();
322      updateTitleImage("Flyout");
323      // add listeners
324      addListener(SWT.Dispose, new Listener() {
325        @Override
326        public void handleEvent(Event event) {
327          if (m_titleImage != null) {
328            m_titleImage.dispose();
329            m_titleImageRotated.dispose();
330            m_titleImage = null;
331            m_titleImageRotated = null;
332          }
333          if (m_backImage != null) {
334            m_backImage.dispose();
335            m_backImage = null;
336          }
337        }
338      });
339      {
340        Listener listener = new Listener() {
341          @Override
342        public void handleEvent(Event event) {
343            layout();
344          }
345        };
346        addListener(SWT.Move, listener);
347        addListener(SWT.Resize, listener);
348      }
349      addListener(SWT.Paint, new Listener() {
350        @Override
351        public void handleEvent(Event event) {
352          handlePaint(event.gc);
353        }
354      });
355      // mouse listeners
356      addMouseListener(new MouseAdapter() {
357        @Override
358        public void mouseDown(MouseEvent event) {
359          if (event.button == 1) {
360            handle_mouseDown(event);
361          }
362        }
363
364        @Override
365        public void mouseUp(MouseEvent event) {
366          if (event.button == 1) {
367            handle_mouseUp(event);
368          }
369        }
370      });
371      addMouseTrackListener(new MouseTrackAdapter() {
372        @Override
373        public void mouseExit(MouseEvent e) {
374          m_stateHover = false;
375          redraw();
376          setCursor(null);
377        }
378
379        @Override
380        public void mouseHover(MouseEvent e) {
381          handle_mouseHover();
382        }
383      });
384      addMouseMoveListener(new MouseMoveListener() {
385        @Override
386        public void mouseMove(MouseEvent event) {
387          handle_mouseMove(event);
388        }
389      });
390    }
391
392    // BEGIN ADT MODIFICATIONS
393    private void dismissHover() {
394      int state = m_preferences.getState();
395      if (state == IFlyoutPreferences.STATE_EXPANDED) {
396        state = IFlyoutPreferences.STATE_COLLAPSED;
397        m_preferences.setState(state);
398        redraw();
399        FlyoutControlComposite.this.layout();
400        if (m_listener != null) {
401            m_listener.stateChanged(IFlyoutPreferences.STATE_EXPANDED, state);
402        }
403      }
404    }
405    // END END MODIFICATIONS
406
407    ////////////////////////////////////////////////////////////////////////////
408    //
409    // Events: mouse
410    //
411    ////////////////////////////////////////////////////////////////////////////
412    private boolean m_resize;
413    private boolean m_stateHover;
414
415    /**
416     * Handler for {@link SWT#MouseDown} event.
417     */
418    private void handle_mouseDown(MouseEvent event) {
419      if (m_stateHover) {
420        int state = m_preferences.getState();
421        // BEGIN ADT MODIFICATIONS
422        int oldState = state;
423        // END ADT MODIFICATIONS
424        if (state == IFlyoutPreferences.STATE_OPEN) {
425          state = IFlyoutPreferences.STATE_COLLAPSED;
426        } else {
427          state = IFlyoutPreferences.STATE_OPEN;
428        }
429        m_preferences.setState(state);
430        redraw();
431        FlyoutControlComposite.this.layout();
432        // BEGIN ADT MODIFICATIONS
433        if (m_listener != null) {
434          m_listener.stateChanged(oldState, state);
435        }
436        // END ADT MODIFICATIONS
437      } else if (getCursor() == ICursorConstants.SIZEWE || getCursor() == ICursorConstants.SIZENS) {
438        m_resize = true;
439      } else if (getCursor() == ICursorConstants.SIZEALL) {
440        handleDocking();
441      }
442    }
443
444    /**
445     * Handler for {@link SWT#MouseUp} event.
446     */
447    private void handle_mouseUp(MouseEvent event) {
448      if (m_resize) {
449        m_resize = false;
450        handle_mouseMove(event);
451      }
452    }
453
454    /**
455     * Handler for {@link SWT#MouseMove} event.
456     */
457    private void handle_mouseMove(MouseEvent event) {
458      final FlyoutControlComposite container = FlyoutControlComposite.this;
459      if (m_resize) {
460        // prepare width
461        int width;
462        if (isHorizontal()) {
463          width = getSize().x;
464        } else {
465          width = getSize().y;
466        }
467        // prepare new width
468        int newWidth = width;
469        if (isWest()) {
470          newWidth = event.x + RESIZE_WIDTH / 2;
471        } else if (isEast()) {
472          newWidth = width - event.x + RESIZE_WIDTH / 2;
473        } else if (isNorth()) {
474          newWidth = event.y + RESIZE_WIDTH / 2;
475        } else if (isSouth()) {
476          newWidth = width - event.y + RESIZE_WIDTH / 2;
477        }
478        // update width
479        if (newWidth != width) {
480          m_preferences.setWidth(newWidth);
481          redraw();
482          container.layout();
483        }
484      } else {
485        Rectangle clientArea = getClientArea();
486        boolean inside = clientArea.contains(event.x, event.y);
487        int x = event.x;
488        int y = event.y;
489        if (inside) {
490          // check for state
491          {
492            boolean oldStateHover = m_stateHover;
493            if (isEast()) {
494              m_stateHover = x > clientArea.width - m_titleHeight && y < m_titleHeight;
495            } else {
496              m_stateHover = x < m_titleHeight && y < m_titleHeight;
497            }
498            if (m_stateHover != oldStateHover) {
499              redraw();
500            }
501            if (m_stateHover) {
502              setCursor(null);
503              return;
504            }
505          }
506          // check for resize band
507          if (isOpenExpanded()) {
508            if (isWest() && x >= clientArea.width - RESIZE_WIDTH) {
509              setCursor(ICursorConstants.SIZEWE);
510            } else if (isEast() && x <= RESIZE_WIDTH) {
511              setCursor(ICursorConstants.SIZEWE);
512            } else if (isNorth() && y >= clientArea.height - RESIZE_WIDTH) {
513              setCursor(ICursorConstants.SIZENS);
514            } else if (isSouth() && y <= RESIZE_WIDTH) {
515              setCursor(ICursorConstants.SIZENS);
516            } else {
517              setCursor(null);
518            }
519          }
520          // check for docking
521          if (getCursor() == null) {
522            setCursor(ICursorConstants.SIZEALL);
523          }
524        } else {
525          setCursor(null);
526        }
527      }
528    }
529
530    /**
531     * Handler for {@link SWT#MouseHover} event - temporary expands flyout and collapse again when
532     * mouse moves above client.
533     */
534    private void handle_mouseHover() {
535      if (m_preferences.getState() == IFlyoutPreferences.STATE_COLLAPSED && !m_stateHover) {
536        m_preferences.setState(IFlyoutPreferences.STATE_EXPANDED);
537        //
538        final FlyoutControlComposite container = FlyoutControlComposite.this;
539        container.layout();
540        // BEGIN ADT MODIFICATIONS
541        if (m_listener != null) {
542            m_listener.stateChanged(IFlyoutPreferences.STATE_COLLAPSED,
543                    IFlyoutPreferences.STATE_EXPANDED);
544        }
545        // END ADT MODIFICATIONS
546        // add listeners
547        Listener listener = new Listener() {
548          @Override
549        public void handleEvent(Event event) {
550            if (event.type == SWT.Dispose) {
551              getDisplay().removeFilter(SWT.MouseMove, this);
552            } else {
553              Point p = ((Control) event.widget).toDisplay(event.x, event.y);
554              // during resize mouse can be temporary outside of flyout - ignore
555              if (m_resize) {
556                return;
557              }
558              // mouse in in flyout container - ignore
559              if (getClientArea().contains(toControl(p.x, p.y))) {
560                return;
561              }
562              // mouse is in full container - collapse
563              if (container.getClientArea().contains(container.toControl(p.x, p.y))) {
564                getDisplay().removeFilter(SWT.MouseMove, this);
565                // it is possible, that user restored (OPEN) flyout, so collapse only if we still in expand state
566                if (m_preferences.getState() == IFlyoutPreferences.STATE_EXPANDED) {
567                  m_preferences.setState(IFlyoutPreferences.STATE_COLLAPSED);
568                  container.layout();
569                  // BEGIN ADT MODIFICATIONS
570                  if (m_listener != null) {
571                      m_listener.stateChanged(IFlyoutPreferences.STATE_EXPANDED,
572                              IFlyoutPreferences.STATE_COLLAPSED);
573                  }
574                  // END ADT MODIFICATIONS
575                }
576              }
577            }
578          }
579        };
580        addListener(SWT.Dispose, listener);
581        getDisplay().addFilter(SWT.MouseMove, listener);
582      }
583    }
584
585    /**
586     * Handler for docking.
587     */
588    private void handleDocking() {
589      final FlyoutControlComposite container = FlyoutControlComposite.this;
590      final int width = m_preferences.getWidth();
591      final int oldDockLocation = getDockLocation();
592      final int[] newDockLocation = new int[]{oldDockLocation};
593      final Tracker dockingTracker = new Tracker(container, SWT.NONE);
594      dockingTracker.setRectangles(new Rectangle[]{getBounds()});
595      dockingTracker.setStippled(true);
596      dockingTracker.addListener(SWT.Move, new Listener() {
597        @Override
598        public void handleEvent(Event event2) {
599          Rectangle clientArea = container.getClientArea();
600          Point location = container.toControl(event2.x, event2.y);
601          int h3 = clientArea.height / 3;
602          // check locations
603          if (location.y < h3 && isValidDockLocation(IFlyoutPreferences.DOCK_NORTH)) {
604            dockingTracker.setRectangles(new Rectangle[]{new Rectangle(0,
605                0,
606                clientArea.width,
607                width)});
608            newDockLocation[0] = IFlyoutPreferences.DOCK_NORTH;
609          } else if (location.y > 2 * h3 && isValidDockLocation(IFlyoutPreferences.DOCK_SOUTH)) {
610            dockingTracker.setRectangles(new Rectangle[]{new Rectangle(0,
611                clientArea.height - width,
612                clientArea.width,
613                width)});
614            newDockLocation[0] = IFlyoutPreferences.DOCK_SOUTH;
615          } else if (location.x < clientArea.width / 2
616              && isValidDockLocation(IFlyoutPreferences.DOCK_WEST)) {
617            dockingTracker.setRectangles(new Rectangle[]{new Rectangle(0,
618                0,
619                width,
620                clientArea.height)});
621            newDockLocation[0] = IFlyoutPreferences.DOCK_WEST;
622          } else if (isValidDockLocation(IFlyoutPreferences.DOCK_EAST)) {
623            dockingTracker.setRectangles(new Rectangle[]{new Rectangle(clientArea.width - width,
624                0,
625                width,
626                clientArea.height)});
627            newDockLocation[0] = IFlyoutPreferences.DOCK_EAST;
628          } else {
629            dockingTracker.setRectangles(new Rectangle[]{getBounds()});
630            newDockLocation[0] = oldDockLocation;
631          }
632        }
633      });
634      // start tracking
635      if (dockingTracker.open()) {
636        setDockLocation(newDockLocation[0]);
637      }
638      // dispose tracker
639      dockingTracker.dispose();
640    }
641
642    ////////////////////////////////////////////////////////////////////////////
643    //
644    // Access
645    //
646    ////////////////////////////////////////////////////////////////////////////
647    /**
648     * @return the {@link Control} installed on this {@link FlyoutControlComposite}, or
649     *         <code>null</code> if there are no any {@link Control}.
650     */
651    private Control getControl() {
652      Control[] children = getChildren();
653      return children.length == 1 ? children[0] : null;
654    }
655
656    /**
657     * Sets the text of title.
658     */
659    public void setTitleText(String text) {
660      updateTitleImage(text);
661    }
662
663    ////////////////////////////////////////////////////////////////////////////
664    //
665    // Layout
666    //
667    ////////////////////////////////////////////////////////////////////////////
668    @Override
669    public void layout() {
670      Control control = getControl();
671      if (control == null) {
672        return;
673      }
674      // OK, we have control, so can continue layout
675      Rectangle clientArea = getClientArea();
676      if (isOpenExpanded()) {
677        if (isWest()) {
678          int y = m_titleHeight;
679          control.setBounds(0, y, clientArea.width - RESIZE_WIDTH, clientArea.height - y);
680        } else if (isEast()) {
681          int y = m_titleHeight;
682          control.setBounds(RESIZE_WIDTH, y, clientArea.width - RESIZE_WIDTH, clientArea.height - y);
683        } else if (isNorth()) {
684          int y = m_titleHeight;
685          control.setBounds(0, y, clientArea.width, clientArea.height - y - RESIZE_WIDTH);
686        } else if (isSouth()) {
687          int y = RESIZE_WIDTH + m_titleHeight;
688          control.setBounds(0, y, clientArea.width, clientArea.height - y);
689        }
690      } else {
691        control.setBounds(0, 0, 0, 0);
692      }
693    }
694
695    ////////////////////////////////////////////////////////////////////////////
696    //
697    // Paint
698    //
699    ////////////////////////////////////////////////////////////////////////////
700    private Image m_backImage;
701
702    /**
703     * Handler for {@link SWT#Paint} event.
704     */
705    private void handlePaint(GC paintGC) {
706      Rectangle clientArea = getClientArea();
707      // prepare back image
708      GC gc;
709      {
710        if (m_backImage == null || !m_backImage.getBounds().equals(clientArea)) {
711          if (m_backImage != null) {
712            m_backImage.dispose();
713          }
714          m_backImage = new Image(getDisplay(), clientArea.width, clientArea.height);
715        }
716        // prepare GC
717        gc = new GC(m_backImage);
718        gc.setBackground(paintGC.getBackground());
719        gc.setForeground(paintGC.getForeground());
720        gc.fillRectangle(clientArea);
721      }
722      //
723      if (isOpenExpanded()) {
724        // draw header
725        {
726          // draw title
727          if (isWest()) {
728            drawStateImage(gc, 0, 0);
729            gc.drawImage(m_titleImage, m_titleHeight, 0);
730          } else if (isEast()) {
731            int x = clientArea.width - m_titleHeight;
732            drawStateImage(gc, x, 0);
733            gc.drawImage(m_titleImage, x - m_titleWidth, 0);
734          } else if (isNorth()) {
735            drawStateImage(gc, 0, 0);
736            gc.drawImage(m_titleImage, m_titleHeight, 0);
737          } else if (isSouth()) {
738            int y = RESIZE_WIDTH;
739            drawStateImage(gc, 0, y);
740            gc.drawImage(m_titleImage, m_titleHeight, y);
741          }
742        }
743        // draw resize band
744        drawResizeBand(gc);
745      } else {
746        if (isHorizontal()) {
747          drawStateImage(gc, 0, 0);
748          gc.drawImage(m_titleImageRotated, 0, m_titleHeight);
749        } else {
750          drawStateImage(gc, 0, 0);
751          gc.drawImage(m_titleImage, m_titleHeight, 0);
752        }
753        DrawUtils.drawHighlightRectangle(gc, 0, 0, clientArea.width, clientArea.height);
754      }
755      // flush back image
756      {
757        gc.dispose();
758        paintGC.drawImage(m_backImage, 0, 0);
759      }
760    }
761
762    /**
763     * Draws the state image (arrow) at given location.
764     */
765    private void drawStateImage(GC gc, int x, int y) {
766      DrawUtils.drawImageCHCV(gc, getStateImage(), x, y, m_titleHeight, m_titleHeight);
767      if (m_stateHover) {
768        DrawUtils.drawHighlightRectangle(gc, x, y, m_titleHeight, m_titleHeight);
769      }
770    }
771
772    /**
773     * @return the {@link Image} corresponding to current state (open or collapsed).
774     */
775    private Image getStateImage() {
776      int location = getDockLocation();
777      int state = m_preferences.getState();
778      if (state == IFlyoutPreferences.STATE_OPEN) {
779        switch (location) {
780          case IFlyoutPreferences.DOCK_WEST :
781            return ARROW_LEFT;
782          case IFlyoutPreferences.DOCK_EAST :
783            return ARROW_RIGHT;
784          case IFlyoutPreferences.DOCK_NORTH :
785            return ARROW_TOP;
786          case IFlyoutPreferences.DOCK_SOUTH :
787            return ARROW_BOTTOM;
788        }
789      } else if (state == IFlyoutPreferences.STATE_EXPANDED) {
790        return PIN;
791      } else {
792        switch (location) {
793          case IFlyoutPreferences.DOCK_WEST :
794            return ARROW_RIGHT;
795          case IFlyoutPreferences.DOCK_EAST :
796            return ARROW_LEFT;
797          case IFlyoutPreferences.DOCK_NORTH :
798            return ARROW_BOTTOM;
799          case IFlyoutPreferences.DOCK_SOUTH :
800            return ARROW_TOP;
801        }
802      }
803      //
804      return null;
805    }
806
807    /**
808     * Draws that resize band, {@link Sash} like.
809     */
810    private void drawResizeBand(GC gc) {
811      Rectangle clientArea = getClientArea();
812      // prepare locations
813      int x, y, width, height;
814      if (isHorizontal()) {
815        if (isWest()) {
816          x = clientArea.width - RESIZE_WIDTH;
817        } else {
818          x = 0;
819        }
820        y = 0;
821        width = RESIZE_WIDTH;
822        height = clientArea.height;
823      } else {
824        x = 0;
825        if (isNorth()) {
826          y = clientArea.height - RESIZE_WIDTH;
827        } else {
828          y = 0;
829        }
830        width = clientArea.width;
831        height = RESIZE_WIDTH;
832      }
833      // draw band
834      DrawUtils.drawHighlightRectangle(gc, x, y, width, height);
835    }
836
837    /**
838     * @return <code>true</code> if flyout is open or expanded.
839     */
840    private boolean isOpenExpanded() {
841      int state = m_preferences.getState();
842      return state == IFlyoutPreferences.STATE_OPEN || state == IFlyoutPreferences.STATE_EXPANDED;
843    }
844
845    ////////////////////////////////////////////////////////////////////////////
846    //
847    // Title image
848    //
849    ////////////////////////////////////////////////////////////////////////////
850    private int m_titleWidth;
851    private int m_titleHeight;
852    private Image m_titleImage;
853    private Image m_titleImageRotated;
854
855    /**
856     * Creates {@link Image} for given title text.
857     */
858    private void updateTitleImage(String text) {
859      // prepare size of text
860      Point textSize;
861      {
862        GC gc = new GC(this);
863        gc.setFont(TITLE_FONT);
864        textSize = gc.textExtent(text);
865        gc.dispose();
866      }
867      // dispose existing image
868      if (m_titleImage != null) {
869        m_titleImage.dispose();
870        m_titleImageRotated.dispose();
871      }
872      // prepare new image
873      {
874        m_titleWidth = textSize.x + 2 * TITLE_LINES + 4 * TITLE_MARGIN;
875        m_titleHeight = textSize.y;
876        m_titleImage = new Image(getDisplay(), m_titleWidth, m_titleHeight);
877        GC gc = new GC(m_titleImage);
878        try {
879          gc.setBackground(getBackground());
880          gc.fillRectangle(0, 0, m_titleWidth, m_titleHeight);
881          int x = 0;
882          // draw left lines
883          {
884            x += TITLE_MARGIN;
885            drawTitleLines(gc, x, m_titleHeight, TITLE_LINES);
886            x += TITLE_LINES + TITLE_MARGIN;
887          }
888          // draw text
889          {
890            gc.setForeground(IColorConstants.black);
891            gc.setFont(TITLE_FONT);
892            gc.drawText(text, x, 0);
893            x += textSize.x;
894          }
895          // draw right lines
896          {
897            x += TITLE_MARGIN;
898            drawTitleLines(gc, x, m_titleHeight, TITLE_LINES);
899          }
900        } finally {
901          gc.dispose();
902        }
903      }
904      // prepare rotated image
905      m_titleImageRotated = DrawUtils.createRotatedImage(m_titleImage);
906    }
907
908    /**
909     * Draws two title lines.
910     */
911    private void drawTitleLines(GC gc, int x, int height, int width) {
912      drawTitleLine(gc, x, height / 3, width);
913      drawTitleLine(gc, x, 2 * height / 3, width);
914    }
915
916    /**
917     * Draws single title line.
918     */
919    private void drawTitleLine(GC gc, int x, int y, int width) {
920      int right = x + TITLE_LINES;
921      //
922      gc.setForeground(IColorConstants.buttonLightest);
923      gc.drawLine(x, y, right - 2, y);
924      gc.drawLine(x, y + 1, right - 2, y + 1);
925      //
926      gc.setForeground(IColorConstants.buttonDarker);
927      gc.drawLine(right - 2, y, right - 1, y);
928      gc.drawLine(x + 2, y + 1, right - 2, y + 1);
929    }
930
931    ////////////////////////////////////////////////////////////////////////////
932    //
933    // Menu
934    //
935    ////////////////////////////////////////////////////////////////////////////
936    private void configureMenu() {
937      final MenuManager manager = new MenuManager();
938      manager.setRemoveAllWhenShown(true);
939      manager.addMenuListener(new IMenuListener() {
940        @Override
941        public void menuAboutToShow(IMenuManager menuMgr) {
942          addDockActions();
943          for (IFlyoutMenuContributor contributor : m_menuContributors) {
944            contributor.contribute(manager);
945          }
946        }
947
948        private void addDockActions() {
949          MenuManager dockManager = new MenuManager(Messages.FlyoutControlComposite_dockManager);
950          addDockAction(
951              dockManager,
952              Messages.FlyoutControlComposite_dockLeft,
953              IFlyoutPreferences.DOCK_WEST);
954          addDockAction(
955              dockManager,
956              Messages.FlyoutControlComposite_dockRight,
957              IFlyoutPreferences.DOCK_EAST);
958          addDockAction(
959              dockManager,
960              Messages.FlyoutControlComposite_dockTop,
961              IFlyoutPreferences.DOCK_NORTH);
962          addDockAction(
963              dockManager,
964              Messages.FlyoutControlComposite_dockBottom,
965              IFlyoutPreferences.DOCK_SOUTH);
966          manager.add(dockManager);
967        }
968
969        private void addDockAction(MenuManager dockManager, String text, int location) {
970          if ((m_validDockLocations & location) != 0) {
971            dockManager.add(new DockAction(text, location));
972          }
973        }
974      });
975      // set menu
976      setMenu(manager.createContextMenu(this));
977      // dispose it later
978      addDisposeListener(new DisposeListener() {
979        @Override
980        public void widgetDisposed(DisposeEvent e) {
981          manager.dispose();
982        }
983      });
984    }
985  }
986  ////////////////////////////////////////////////////////////////////////////
987  //
988  // DockAction
989  //
990  ////////////////////////////////////////////////////////////////////////////
991  private class DockAction extends Action {
992    private final int m_location;
993
994    ////////////////////////////////////////////////////////////////////////////
995    //
996    // Constructor
997    //
998    ////////////////////////////////////////////////////////////////////////////
999    public DockAction(String text, int location) {
1000      super(text, AS_RADIO_BUTTON);
1001      m_location = location;
1002    }
1003
1004    ////////////////////////////////////////////////////////////////////////////
1005    //
1006    // Action
1007    //
1008    ////////////////////////////////////////////////////////////////////////////
1009    @Override
1010    public boolean isChecked() {
1011      return getDockLocation() == m_location;
1012    }
1013
1014    @Override
1015    public void run() {
1016      setDockLocation(m_location);
1017    }
1018  }
1019}
1020