1 /*******************************************************************************
2  * Copyright (c) 2000, 2016 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.ui.forms.widgets;
15 import org.eclipse.swt.SWT;
16 import org.eclipse.swt.accessibility.ACC;
17 import org.eclipse.swt.accessibility.AccessibleAdapter;
18 import org.eclipse.swt.accessibility.AccessibleControlAdapter;
19 import org.eclipse.swt.accessibility.AccessibleControlEvent;
20 import org.eclipse.swt.accessibility.AccessibleEvent;
21 import org.eclipse.swt.graphics.Color;
22 import org.eclipse.swt.graphics.Point;
23 import org.eclipse.swt.graphics.Rectangle;
24 import org.eclipse.swt.widgets.Composite;
25 import org.eclipse.swt.widgets.Event;
26 import org.eclipse.swt.widgets.Listener;
27 import org.eclipse.ui.forms.events.HyperlinkAdapter;
28 import org.eclipse.ui.forms.events.HyperlinkEvent;
29 import org.eclipse.ui.internal.forms.Messages;
30 
31 /**
32  * A custom selectable control that can be used to control areas that can be
33  * expanded or collapsed.
34  * <p>
35  * This is an abstract class. Subclasses are responsible for rendering the
36  * control using decoration and hover decoration color. Control should be
37  * rendered based on the current expansion state.
38  * </p>
39  * <dl>
40  * <dt><b>Styles:</b></dt>
41  * <dd>None</dd>
42  * </dl>
43  *
44  * @since 3.0
45  */
46 public abstract class ToggleHyperlink extends AbstractHyperlink {
47 	protected int innerWidth;
48 	protected int innerHeight;
49 	protected boolean hover;
50 	private boolean expanded;
51 	private Color decorationColor;
52 	private Color hoverColor;
53 	/**
54 	 * Creates a control in a provided composite.
55 	 *
56 	 * @param parent
57 	 *            the parent
58 	 * @param style
59 	 *            the style
60 	 */
ToggleHyperlink(Composite parent, int style)61 	public ToggleHyperlink(Composite parent, int style) {
62 		super(parent, style);
63 		Listener listener = e -> {
64 			switch (e.type) {
65 			case SWT.MouseEnter:
66 				hover = true;
67 				redraw();
68 				break;
69 			case SWT.MouseExit:
70 				hover = false;
71 				redraw();
72 				break;
73 			case SWT.KeyDown:
74 				onKeyDown(e);
75 				break;
76 			}
77 		};
78 		addListener(SWT.MouseEnter, listener);
79 		addListener(SWT.MouseExit, listener);
80 		addListener(SWT.KeyDown, listener);
81 		addHyperlinkListener(new HyperlinkAdapter() {
82 			@Override
83 			public void linkActivated(HyperlinkEvent e) {
84 				setExpanded(!isExpanded());
85 			}
86 		});
87 		initAccessible();
88 	}
89 
90 	/**
91 	 * Sets the color of the decoration.
92 	 *
93 	 * @param decorationColor color to set
94 	 */
setDecorationColor(Color decorationColor)95 	public void setDecorationColor(Color decorationColor) {
96 		this.decorationColor = decorationColor;
97 	}
98 	/**
99 	 * Returns the color of the decoration.
100 	 *
101 	 * @return decoration color
102 	 */
getDecorationColor()103 	public Color getDecorationColor() {
104 		return decorationColor;
105 	}
106 	/**
107 	 * Sets the hover color of decoration. Hover color will be used when mouse
108 	 * enters the decoration area.
109 	 *
110 	 * @param hoverColor
111 	 *            the hover color to use
112 	 */
setHoverDecorationColor(Color hoverColor)113 	public void setHoverDecorationColor(Color hoverColor) {
114 		this.hoverColor = hoverColor;
115 	}
116 	/**
117 	 * Returns the hover color of the decoration.
118 	 *
119 	 * @return the hover color of the decoration.
120 	 * @since 3.1
121 	 */
getHoverDecorationColor()122 	public Color getHoverDecorationColor() {
123 		return hoverColor;
124 	}
125 
126 	/**
127 	 * Returns the hover color of the decoration.
128 	 *
129 	 * @return the hover color of the decoration.
130 	 * @deprecated use <code>getHoverDecorationColor</code>
131 	 * @see #getHoverDecorationColor()
132 	 */
133 	@Deprecated
geHoverDecorationColor()134 	public Color geHoverDecorationColor() {
135 		return hoverColor;
136 	}
137 	/**
138 	 * Computes the size of the control.
139 	 *
140 	 * @param wHint
141 	 *            width hint
142 	 * @param hHint
143 	 *            height hint
144 	 * @param changed
145 	 *            if true, flush any saved layout state
146 	 */
147 	@Override
computeSize(int wHint, int hHint, boolean changed)148 	public Point computeSize(int wHint, int hHint, boolean changed) {
149 		int width = innerWidth + 2 * marginWidth;
150 		int height = innerHeight + 2 * marginHeight;
151 		if (wHint != SWT.DEFAULT)
152 			width = wHint;
153 		if (hHint != SWT.DEFAULT)
154 			height = hHint;
155 
156 		Rectangle trim = computeTrim(0, 0, width, height);
157 		return new Point(trim.width, trim.height);
158 	}
159 	/**
160 	 * Returns the expansion state of the toggle control. When toggle is in the
161 	 * normal (downward) state, the value is <samp>true </samp>. Collapsed
162 	 * control will return <samp>false </samp>.
163 	 *
164 	 * @return <samp>false </samp> if collapsed, <samp>true </samp> otherwise.
165 	 */
isExpanded()166 	public boolean isExpanded() {
167 		return expanded;
168 	}
169 	/**
170 	 * Sets the expansion state of the twistie control
171 	 *
172 	 * @param expanded the expansion state
173 	 */
setExpanded(boolean expanded)174 	public void setExpanded(boolean expanded) {
175 		this.expanded = expanded;
176 		redraw();
177 	}
initAccessible()178 	private void initAccessible() {
179 		getAccessible().addAccessibleListener(new AccessibleAdapter() {
180 			@Override
181 			public void getHelp(AccessibleEvent e) {
182 				e.result = getToolTipText();
183 			}
184 			@Override
185 			public void getName(AccessibleEvent e) {
186 				e.result = Messages.ToggleHyperlink_accessibleName;
187 			}
188 			@Override
189 			public void getDescription(AccessibleEvent e) {
190 				getName(e);
191 			}
192 		});
193 		getAccessible().addAccessibleControlListener(
194 				new AccessibleControlAdapter() {
195 					@Override
196 					public void getChildAtPoint(AccessibleControlEvent e) {
197 						Point testPoint = toControl(new Point(e.x, e.y));
198 						if (getBounds().contains(testPoint)) {
199 							e.childID = ACC.CHILDID_SELF;
200 						}
201 					}
202 					@Override
203 					public void getLocation(AccessibleControlEvent e) {
204 						Rectangle location = getBounds();
205 						Point pt = toDisplay(new Point(location.x, location.y));
206 						e.x = pt.x;
207 						e.y = pt.y;
208 						e.width = location.width;
209 						e.height = location.height;
210 					}
211 					@Override
212 					public void getSelection (AccessibleControlEvent e) {
213 						if (ToggleHyperlink.this.getSelection())
214 							e.childID = ACC.CHILDID_SELF;
215 					}
216 
217 					@Override
218 					public void getFocus (AccessibleControlEvent e) {
219 						if (ToggleHyperlink.this.getSelection())
220 							e.childID = ACC.CHILDID_SELF;
221 					}
222 					@Override
223 					public void getChildCount(AccessibleControlEvent e) {
224 						e.detail = 0;
225 					}
226 					@Override
227 					public void getRole(AccessibleControlEvent e) {
228 						e.detail = ACC.ROLE_TREE;
229 					}
230 					@Override
231 					public void getState(AccessibleControlEvent e) {
232 						e.detail = ToggleHyperlink.this.isExpanded()
233 								? ACC.STATE_EXPANDED
234 								: ACC.STATE_COLLAPSED;
235 					}
236 					@Override
237 					public void getValue(AccessibleControlEvent e) {
238 						if (e.childID == ACC.CHILDID_SELF) {
239 							String name = Messages.ToggleHyperlink_accessibleName;
240 							if (getParent() instanceof ExpandableComposite) {
241 								name = Messages.ToggleHyperlink_accessibleColumn+((ExpandableComposite)getParent()).getText();
242 								int index = name.indexOf('&');
243 								if (index != -1) {
244 									name = name.substring(0, index) + name.substring(index + 1);
245 								}
246 							}
247 							e.result = name;
248 						}
249 					}
250 				});
251 	}
252 	// set bogus childIDs on link activation to ensure state is read on expand/collapse
253 	@Override
triggerAccessible()254 	void triggerAccessible() {
255 		getAccessible().setFocus(getAccessibleChildID());
256 	}
getAccessibleChildID()257 	private int getAccessibleChildID() {
258 		return ToggleHyperlink.this.isExpanded() ? 1 : 2;
259 	}
260 
onKeyDown(Event e)261 	private void onKeyDown(Event e) {
262 		if (e.keyCode==SWT.ARROW_RIGHT) {
263 			// expand if collapsed
264 			if (!isExpanded()) {
265 				handleActivate(e);
266 			}
267 			e.doit=false;
268 		}
269 		else if (e.keyCode==SWT.ARROW_LEFT) {
270 			// collapse if expanded
271 			if (isExpanded()) {
272 				handleActivate(e);
273 			}
274 			e.doit=false;
275 		}
276 	}
277 }
278