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  *******************************************************************************/
11 package org.eclipse.wb.core.controls.flyout;
12 
13 import org.eclipse.jface.action.Action;
14 import org.eclipse.jface.action.IMenuListener;
15 import org.eclipse.jface.action.IMenuManager;
16 import org.eclipse.jface.action.MenuManager;
17 import org.eclipse.jface.resource.JFaceResources;
18 import org.eclipse.swt.SWT;
19 import org.eclipse.swt.events.DisposeEvent;
20 import org.eclipse.swt.events.DisposeListener;
21 import org.eclipse.swt.events.MouseAdapter;
22 import org.eclipse.swt.events.MouseEvent;
23 import org.eclipse.swt.events.MouseMoveListener;
24 import org.eclipse.swt.events.MouseTrackAdapter;
25 import org.eclipse.swt.graphics.Font;
26 import org.eclipse.swt.graphics.GC;
27 import org.eclipse.swt.graphics.Image;
28 import org.eclipse.swt.graphics.Point;
29 import org.eclipse.swt.graphics.Rectangle;
30 import org.eclipse.swt.widgets.Composite;
31 import org.eclipse.swt.widgets.Control;
32 import org.eclipse.swt.widgets.Event;
33 import org.eclipse.swt.widgets.Listener;
34 import org.eclipse.swt.widgets.Tracker;
35 import org.eclipse.wb.core.controls.Messages;
36 import org.eclipse.wb.draw2d.IColorConstants;
37 import org.eclipse.wb.draw2d.ICursorConstants;
38 import org.eclipse.wb.internal.core.utils.ui.DrawUtils;
39 
40 import java.util.ArrayList;
41 import 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  */
51 public 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 
loadImage(String path)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   ////////////////////////////////////////////////////////////////////////////
FlyoutControlComposite(Composite parent, int style, IFlyoutPreferences preferences)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    */
getFlyoutParent()114   public Composite getFlyoutParent() {
115     return m_flyoutContainer;
116   }
117 
118   /**
119    * @return the parent {@link Composite} for client {@link Control}.
120    */
getClientParent()121   public Composite getClientParent() {
122     return this;
123   }
124 
125   /**
126    * Sets the bit set with valid docking locations.
127    */
setValidDockLocations(int validDockLocations)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    */
setMinWidth(int minWidth)140   public void setMinWidth(int minWidth) {
141     m_minWidth = minWidth;
142   }
143 
144   /**
145    * Sets the text of title.
146    */
setTitleText(String text)147   public void setTitleText(String text) {
148     m_flyoutContainer.setTitleText(text);
149   }
150 
151   /**
152    * Adds new {@link IFlyoutMenuContributor}.
153    */
addMenuContributor(IFlyoutMenuContributor contributor)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
layout()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   ////////////////////////////////////////////////////////////////////////////
isHorizontal()232   private boolean isHorizontal() {
233     return isWest() || isEast();
234   }
235 
isWest()236   private boolean isWest() {
237     return getDockLocation() == IFlyoutPreferences.DOCK_WEST;
238   }
239 
isEast()240   private boolean isEast() {
241     return getDockLocation() == IFlyoutPreferences.DOCK_EAST;
242   }
243 
isNorth()244   private boolean isNorth() {
245     return getDockLocation() == IFlyoutPreferences.DOCK_NORTH;
246   }
247 
isSouth()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    */
isValidDockLocation(int location)255   private boolean isValidDockLocation(int location) {
256     return (location & m_validDockLocations) == location;
257   }
258 
259   /**
260    * @return current docking location.
261    */
getDockLocation()262   private int getDockLocation() {
263     return m_preferences.getDockLocation();
264   }
265 
266   /**
267    * Sets new docking location.
268    */
setDockLocation(int dockLocation)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    */
apply(IFlyoutPreferences preferences)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 */
dismissHover()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 */
setListener(IFlyoutListener listener)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     ////////////////////////////////////////////////////////////////////////////
FlyoutContainer(Composite parent, int style)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
dismissHover()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      */
handle_mouseDown(MouseEvent event)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      */
handle_mouseUp(MouseEvent event)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      */
handle_mouseMove(MouseEvent event)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