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 * Kai Nacke - Fix for Bug 202382 14 * Bryan Hunt - Fix for Bug 245457 15 * Didier Villevalois - Fix for Bug 178534 16 * Robin Stocker - Fix for Bug 193034 (tool tip also on text) 17 * Alena Laskavaia - Bug 481604, Bug 482024 18 * Ralf Petter <ralf.petter@gmail.com> - Bug 183675 19 * Jens Reimann <jreimann@redhat.com> - Bug 520833 20 *******************************************************************************/ 21 package org.eclipse.ui.forms.widgets; 22 23 import org.eclipse.core.runtime.Assert; 24 import org.eclipse.core.runtime.ListenerList; 25 import org.eclipse.swt.SWT; 26 import org.eclipse.swt.events.FocusEvent; 27 import org.eclipse.swt.events.FocusListener; 28 import org.eclipse.swt.events.KeyAdapter; 29 import org.eclipse.swt.events.KeyEvent; 30 import org.eclipse.swt.events.PaintEvent; 31 import org.eclipse.swt.graphics.Color; 32 import org.eclipse.swt.graphics.Font; 33 import org.eclipse.swt.graphics.GC; 34 import org.eclipse.swt.graphics.Point; 35 import org.eclipse.swt.graphics.Rectangle; 36 import org.eclipse.swt.widgets.Canvas; 37 import org.eclipse.swt.widgets.Composite; 38 import org.eclipse.swt.widgets.Control; 39 import org.eclipse.swt.widgets.Label; 40 import org.eclipse.swt.widgets.Layout; 41 import org.eclipse.swt.widgets.Listener; 42 import org.eclipse.swt.widgets.Menu; 43 import org.eclipse.swt.widgets.Shell; 44 import org.eclipse.ui.forms.events.ExpansionEvent; 45 import org.eclipse.ui.forms.events.HyperlinkAdapter; 46 import org.eclipse.ui.forms.events.HyperlinkEvent; 47 import org.eclipse.ui.forms.events.IExpansionListener; 48 import org.eclipse.ui.internal.forms.widgets.FormUtil; 49 import org.eclipse.ui.internal.forms.widgets.FormsResources; 50 51 /** 52 * This composite is capable of expanding or collapsing a single client that is 53 * its direct child. The composite renders an expansion toggle affordance 54 * (according to the chosen style), and a title that also acts as a hyperlink 55 * (can be selected and is traversable). The client is laid out below the title 56 * when expanded, or hidden when collapsed. 57 * <p> 58 * The widget can be instantiated as-is, or subclassed to modify some aspects of 59 * it. * 60 * <p> 61 * Since 3.1, left/right arrow keys can be used to control the expansion state. 62 * If several expandable composites are created in the same parent, up/down 63 * arrow keys can be used to traverse between them. Expandable text accepts 64 * mnemonics and mnemonic activation will toggle the expansion state. 65 * 66 * <p> 67 * While expandable composite recognize that different styles can be used to 68 * render the title bar, and even defines the constants for these styles 69 * (<code>TITLE_BAR</code> and <code>SHORT_TITLE_BAR</code> the actual painting 70 * is done in the subclasses. 71 * 72 * @see Section 73 * @since 3.0 74 */ 75 public class ExpandableComposite extends Canvas { 76 /** 77 * If this style is used, a twistie will be used to render the expansion 78 * toggle. 79 */ 80 public static final int TWISTIE = 1 << 1; 81 82 /** 83 * If this style is used, a tree node with either + or - signs will be used 84 * to render the expansion toggle. 85 */ 86 public static final int TREE_NODE = 1 << 2; 87 88 /** 89 * If this style is used, the title text will be rendered as a hyperlink 90 * that can individually accept focus. Otherwise, it will still act like a 91 * hyperlink, but only the toggle control will accept focus. 92 */ 93 public static final int FOCUS_TITLE = 1 << 3; 94 95 /** 96 * If this style is used, the client origin will be vertically aligned with 97 * the title text. Otherwise, it will start at x = 0. 98 */ 99 public static final int CLIENT_INDENT = 1 << 4; 100 101 /** 102 * If this style is used, computed size of the composite will take the 103 * client width into consideration only in the expanded state. Otherwise, 104 * client width will always be taken into account. 105 */ 106 public static final int COMPACT = 1 << 5; 107 108 /** 109 * If this style is used, the control will be created in the expanded state. 110 * This state can later be changed programmatically or by the user if 111 * TWISTIE or TREE_NODE style is used. 112 */ 113 public static final int EXPANDED = 1 << 6; 114 115 /** 116 * If this style is used, title bar decoration will be painted behind the 117 * text. 118 */ 119 public static final int TITLE_BAR = 1 << 8; 120 121 /** 122 * If this style is used, a short version of the title bar decoration will 123 * be painted behind the text. This style is useful when a more discrete 124 * option is needed for the title bar. 125 * 126 * @since 3.1 127 */ 128 public static final int SHORT_TITLE_BAR = 1 << 9; 129 130 /** 131 * If this style is used, title will not be rendered. 132 */ 133 public static final int NO_TITLE = 1 << 12; 134 135 /** 136 * By default, text client is right-aligned. If this style is used, it will 137 * be positioned after the text control and vertically centered with it. 138 */ 139 public static final int LEFT_TEXT_CLIENT_ALIGNMENT = 1 << 13; 140 141 /** 142 * By default, a focus box is painted around the title when it receives focus. 143 * If this style is used, the focus box will not be painted. This style does 144 * not apply when FOCUS_TITLE is used. 145 * @since 3.5 146 */ 147 public static final int NO_TITLE_FOCUS_BOX = 1 << 14; 148 149 /** 150 * Width of the margin that will be added around the control (default is 0). 151 */ 152 public int marginWidth = 0; 153 154 /** 155 * Height of the margin that will be added around the control (default is 156 * 0). 157 */ 158 public int marginHeight = 0; 159 160 /** 161 * Vertical spacing between the title area and the composite client control 162 * (default is 3). 163 */ 164 public int clientVerticalSpacing = 3; 165 166 /** 167 * Vertical spacing between the title area and the description control 168 * (default is 0). The description control is normally placed at the new 169 * line as defined in the font used to render it. This value will be added 170 * to it. 171 * 172 * @since 3.3 173 */ 174 public int descriptionVerticalSpacing = 0; 175 176 /** 177 * Horizontal margin around the inside of the title bar area when TITLE_BAR 178 * or SHORT_TITLE_BAR style is used. This variable is not used otherwise. 179 * 180 * @since 3.3 181 */ 182 public int titleBarTextMarginWidth = 6; 183 184 /** 185 * The toggle widget used to expand the composite. 186 */ 187 protected ToggleHyperlink toggle; 188 189 /** 190 * The text label for the title. 191 */ 192 protected Control textLabel; 193 194 /** 195 * @deprecated this variable was left as protected by mistake. It will be 196 * turned into static and hidden in the future versions. Do not 197 * use them and do not change its value. 198 */ 199 @Deprecated 200 protected int VGAP = 3; 201 /** 202 * @deprecated this variable was left as protected by mistake. It will be 203 * turned into static and hidden in the future versions. Do not 204 * use it and do not change its value. 205 */ 206 @Deprecated 207 protected int GAP = 4; 208 209 static final int IGAP = 4; 210 static final int IVGAP = 3; 211 212 private static final Point NULL_SIZE = new Point(0, 0); 213 214 private static final int VSPACE = 3; 215 216 private static final int SEPARATOR_HEIGHT = 2; 217 218 private int expansionStyle = TWISTIE | FOCUS_TITLE | EXPANDED; 219 220 private boolean expanded; 221 222 private Control textClient; 223 224 private Control client; 225 226 private ListenerList<IExpansionListener> listeners = new ListenerList<>(); 227 228 private Color titleBarForeground; 229 230 private class ExpandableLayout extends Layout implements ILayoutExtension { 231 232 private static final int MIN_WIDTH = -2; 233 234 private SizeCache toggleCache = new SizeCache(); 235 236 private SizeCache textClientCache = new SizeCache(); 237 238 private SizeCache textLabelCache = new SizeCache(); 239 240 private SizeCache descriptionCache = new SizeCache(); 241 242 private SizeCache clientCache = new SizeCache(); 243 initCache(boolean shouldFlush)244 private void initCache(boolean shouldFlush) { 245 toggleCache.setControl(toggle); 246 textClientCache.setControl(textClient); 247 textLabelCache.setControl(textLabel); 248 descriptionCache.setControl(getDescriptionControl()); 249 clientCache.setControl(client); 250 251 if (shouldFlush) { 252 toggleCache.flush(); 253 textClientCache.flush(); 254 textLabelCache.flush(); 255 descriptionCache.flush(); 256 clientCache.flush(); 257 } 258 } 259 260 @Override layout(Composite parent, boolean changed)261 protected void layout(Composite parent, boolean changed) { 262 initCache(changed); 263 264 Rectangle clientArea = parent.getClientArea(); 265 int thmargin = 0; 266 int tvmargin = 0; 267 268 if (hasTitleBar()) { 269 thmargin = titleBarTextMarginWidth; 270 tvmargin = IVGAP; 271 } 272 int x = marginWidth + thmargin; 273 int y = marginHeight + tvmargin; 274 // toggle 275 Point toggleSize = toggleCache.computeSize(SWT.DEFAULT, SWT.DEFAULT); 276 277 int width = clientArea.width - marginWidth - marginWidth - thmargin - thmargin; 278 if (toggleSize.x > 0) 279 width -= toggleSize.x + IGAP; 280 281 // TODO: This code is common between computeSize and layout 282 int gapBetweenTcAndLabel = (textClient != null && textLabel != null) ? IGAP : 0; 283 284 int widthForTcAndLabel = Math.max(0, width - gapBetweenTcAndLabel); 285 286 Point tcDefault = textClientCache.computeSize(SWT.DEFAULT, SWT.DEFAULT); 287 Point labelDefault = this.textLabelCache.computeSize(SWT.DEFAULT, SWT.DEFAULT); 288 289 int tcWidthBeforeSplit = Math.min(width, tcDefault.x); 290 int labelWidthBeforeSplit = Math.min(width, labelDefault.x); 291 292 int tcWidthAfterSplit = tcWidthBeforeSplit; 293 int labelWidthAfterSplit = labelWidthBeforeSplit; 294 295 int expectedWidthForTcAndLabel = tcWidthBeforeSplit + labelWidthBeforeSplit; 296 297 if (expectedWidthForTcAndLabel > widthForTcAndLabel) { 298 // this is heuristic since we don't have a reliable way to find 299 // out if control can wrap. It checks if width of each label or 300 // textClient is less then half 301 // and gives them what they asked in this case 302 if (labelWidthBeforeSplit < widthForTcAndLabel / 2) { 303 labelWidthAfterSplit = labelWidthBeforeSplit; 304 } else { 305 labelWidthAfterSplit = widthForTcAndLabel * labelWidthBeforeSplit 306 / expectedWidthForTcAndLabel; 307 } 308 309 if (tcWidthBeforeSplit < widthForTcAndLabel / 2) { 310 tcWidthAfterSplit = tcWidthBeforeSplit; 311 labelWidthAfterSplit = widthForTcAndLabel - tcWidthAfterSplit; 312 } else { 313 tcWidthAfterSplit = widthForTcAndLabel - labelWidthAfterSplit; 314 } 315 } 316 317 // TODO: Add support for fill alignment of textControl 318 319 Point tcsize = textClientCache.computeSize(tcWidthAfterSplit, SWT.DEFAULT); 320 Point size = textLabelCache.computeSize(labelWidthAfterSplit, SWT.DEFAULT); 321 322 int height = Math.max(tcsize.y, size.y); // max of label/text client 323 height = Math.max(height, toggleSize.y); // or max of toggle 324 325 boolean leftAlignment = textClient != null && (expansionStyle & LEFT_TEXT_CLIENT_ALIGNMENT) != 0; 326 if (toggle != null) { 327 // if label control is absent we vertically center the toggle, 328 // because the text client is usually a lot thicker 329 int ty = (height - toggleSize.y + 1) / 2 + 1; 330 ty = Math.max(ty, 0); 331 ty += marginHeight + tvmargin; 332 toggle.setLocation(x, ty); 333 toggle.setSize(toggleSize); 334 x += toggleSize.x + IGAP; 335 } 336 if (textLabel != null) { 337 int ty = y; 338 if (leftAlignment) { 339 if (size.y < tcsize.y) 340 ty = (tcsize.y - size.y) / 2 + marginHeight 341 + tvmargin; 342 } 343 344 int gap = 0; 345 if (size.y < height) { 346 // bug 520833: the text label is smaller in height 347 // than the composite so we align it to the middle 348 gap = (height - size.y) / 2; 349 } 350 351 textLabelCache.setBounds(x, ty + gap, size.x, size.y); 352 } 353 354 if (textClient != null) { 355 int tcwidth = clientArea.width - marginWidth - marginWidth - thmargin - thmargin; 356 if (toggleSize.x > 0) 357 tcwidth -= toggleSize.x + IGAP; 358 if (size.x > 0) 359 tcwidth -= size.x + IGAP; 360 tcwidth = Math.min(tcsize.x, tcwidth); 361 if (tcwidth < 0) 362 tcwidth = 0; 363 int tcx; 364 if ((expansionStyle & LEFT_TEXT_CLIENT_ALIGNMENT) != 0) { 365 tcx = x + ((size.x > 0) ? size.x + IGAP : 0); 366 } else { 367 tcx = clientArea.width - tcwidth - marginWidth - thmargin; 368 } 369 textClientCache.setBounds(tcx, y, tcwidth, height); 370 } 371 372 y += height; 373 if (hasTitleBar()) 374 y += tvmargin; 375 Control separatorControl = getSeparatorControl(); 376 if (separatorControl != null) { 377 y += VSPACE; 378 separatorControl.setBounds(marginWidth, y, 379 clientArea.width - marginWidth - marginWidth, 380 SEPARATOR_HEIGHT); 381 y += SEPARATOR_HEIGHT; 382 } 383 if (expanded && client != null) { 384 int areaWidth = clientArea.width - marginWidth - thmargin; 385 int cx = marginWidth + thmargin; 386 if ((expansionStyle & CLIENT_INDENT) != 0) { 387 cx = x; 388 } 389 areaWidth -= cx; 390 Control desc = getDescriptionControl(); 391 if (desc != null) { 392 if (separatorControl != null) { 393 y += VSPACE; 394 } 395 Point dsize = descriptionCache.computeSize(areaWidth, SWT.DEFAULT); 396 y += descriptionVerticalSpacing; 397 descriptionCache.setBounds(cx, y, areaWidth, dsize.y); 398 y += dsize.y; 399 } 400 y += clientVerticalSpacing; 401 int cwidth = areaWidth; 402 int cheight = clientArea.height - marginHeight - marginHeight - y; 403 clientCache.setBounds(cx, y, cwidth, cheight); 404 } 405 } 406 407 @Override computeSize(Composite parent, int wHint, int hHint, boolean changed)408 protected Point computeSize(Composite parent, int wHint, int hHint, 409 boolean changed) { 410 initCache(changed); 411 412 Point toggleSize = NULL_SIZE; 413 int toggleWidthPlusGap = 0; 414 if (toggle != null) { 415 toggleSize = toggleCache.computeSize(SWT.DEFAULT, SWT.DEFAULT); 416 toggleWidthPlusGap = toggleSize.x + IGAP; 417 } 418 int thmargin = 0; 419 int tvmargin = 0; 420 421 if (hasTitleBar()) { 422 thmargin = titleBarTextMarginWidth; 423 tvmargin = IVGAP; 424 } 425 426 // TODO: This code is common between computeSize and layout 427 int gapBetweenTcAndLabel = (textClient != null && textLabel != null) ? IGAP : 0; 428 429 Point tcDefault = textClientCache.computeSize(SWT.DEFAULT, SWT.DEFAULT); 430 Point labelDefault = this.textLabelCache.computeSize(SWT.DEFAULT, SWT.DEFAULT); 431 432 int width = 0; 433 if (wHint == SWT.DEFAULT || wHint == MIN_WIDTH) { 434 width += toggleWidthPlusGap; 435 width += labelDefault.x; 436 width += gapBetweenTcAndLabel; 437 width += tcDefault.x; 438 } else { 439 width = wHint - marginWidth - marginWidth - thmargin - thmargin; 440 } 441 442 width = Math.max(0, width); 443 444 int widthForTcAndLabel = Math.max(0, width - gapBetweenTcAndLabel - toggleWidthPlusGap); 445 446 int tcWidthBeforeSplit = Math.min(width, tcDefault.x); 447 int labelWidthBeforeSplit = Math.min(width, labelDefault.x); 448 449 int tcWidthAfterSplit = tcWidthBeforeSplit; 450 int labelWidthAfterSplit = labelWidthBeforeSplit; 451 452 int expectedWidthForTcAndLabel = tcWidthBeforeSplit + labelWidthBeforeSplit; 453 454 if (expectedWidthForTcAndLabel > widthForTcAndLabel) { 455 labelWidthAfterSplit = widthForTcAndLabel * labelWidthBeforeSplit / expectedWidthForTcAndLabel; 456 tcWidthAfterSplit = widthForTcAndLabel - labelWidthAfterSplit; 457 } 458 459 // TODO: Add support for fill alignment of textControl 460 461 Point tcsize = textClientCache.computeSize(tcWidthAfterSplit, SWT.DEFAULT); 462 Point size = textLabelCache.computeSize(labelWidthAfterSplit, SWT.DEFAULT); 463 464 int height = Math.max(tcsize.y, size.y); // max of label/text client 465 height = Math.max(height, toggleSize.y); // or max of toggle 466 467 if (getSeparatorControl() != null) { 468 height += VSPACE + SEPARATOR_HEIGHT; 469 } 470 // if (hasTitleBar()) 471 // height += VSPACE; 472 if ((expanded || (expansionStyle & COMPACT) == 0) && client != null) { 473 int cwHint = wHint; 474 int clientIndent = 0; 475 if ((expansionStyle & CLIENT_INDENT) != 0) 476 clientIndent = toggleWidthPlusGap; 477 478 if (cwHint != SWT.DEFAULT && cwHint != MIN_WIDTH) { 479 cwHint -= marginWidth + marginWidth + thmargin + thmargin; 480 if ((expansionStyle & CLIENT_INDENT) != 0) 481 if (tcsize.x > 0) 482 cwHint -= toggleWidthPlusGap; 483 } 484 Point dsize = null; 485 Point csize; 486 if (cwHint == MIN_WIDTH) { 487 int minWidth = clientCache.computeMinimumWidth(); 488 csize = clientCache.computeSize(minWidth, SWT.DEFAULT); 489 } else { 490 csize = clientCache.computeSize(cwHint, SWT.DEFAULT); 491 } 492 if (getDescriptionControl() != null) { 493 int dwHint = cwHint; 494 if (dwHint == SWT.DEFAULT || dwHint == MIN_WIDTH) { 495 dwHint = csize.x; 496 if ((expansionStyle & CLIENT_INDENT) != 0) 497 dwHint -= toggleWidthPlusGap; 498 } 499 dsize = descriptionCache.computeSize(dwHint, SWT.DEFAULT); 500 width = Math.max(width, dsize.x + clientIndent); 501 if (expanded) { 502 if (getSeparatorControl() != null) { 503 height += VSPACE; 504 } 505 height += descriptionVerticalSpacing + dsize.y; 506 } 507 } 508 width = Math.max(width, csize.x + clientIndent); 509 if (expanded) { 510 height += clientVerticalSpacing; 511 height += csize.y; 512 } 513 } 514 515 int resultWidth = width + marginWidth + marginWidth + thmargin + thmargin; 516 517 if (wHint != SWT.DEFAULT && wHint != MIN_WIDTH) { 518 resultWidth = wHint; 519 } 520 521 int resultHeight = height + marginHeight + marginHeight + tvmargin + tvmargin; 522 523 if (hHint != SWT.DEFAULT) { 524 resultHeight = hHint; 525 } 526 527 return new Point(resultWidth, resultHeight); 528 } 529 530 @Override computeMinimumWidth(Composite parent, boolean changed)531 public int computeMinimumWidth(Composite parent, boolean changed) { 532 return computeSize(parent, MIN_WIDTH, SWT.DEFAULT, changed).x; 533 } 534 535 @Override computeMaximumWidth(Composite parent, boolean changed)536 public int computeMaximumWidth(Composite parent, boolean changed) { 537 return computeSize(parent, SWT.DEFAULT, SWT.DEFAULT, changed).x; 538 } 539 } 540 541 /** 542 * Creates an expandable composite using a TWISTIE toggle. 543 * 544 * @param parent 545 * the parent composite 546 * @param style 547 * SWT style bits 548 */ ExpandableComposite(Composite parent, int style)549 public ExpandableComposite(Composite parent, int style) { 550 this(parent, style, TWISTIE); 551 } 552 553 /** 554 * Creates the expandable composite in the provided parent. 555 * 556 * @param parent 557 * the parent 558 * @param style 559 * the control style (as expected by SWT subclass) 560 * @param expansionStyle 561 * the style of the expansion widget (TREE_NODE, TWISTIE, 562 * CLIENT_INDENT, COMPACT, FOCUS_TITLE, 563 * LEFT_TEXT_CLIENT_ALIGNMENT, NO_TITLE) 564 */ ExpandableComposite(Composite parent, int style, int expansionStyle)565 public ExpandableComposite(Composite parent, int style, int expansionStyle) { 566 super(parent, style); 567 this.expansionStyle = expansionStyle; 568 if ((expansionStyle & TITLE_BAR) != 0) 569 setBackgroundMode(SWT.INHERIT_DEFAULT); 570 super.setLayout(new ExpandableLayout()); 571 if (hasTitleBar()) { 572 this.addPaintListener(e -> { 573 if (!isDisposed()) { 574 onPaint(e); 575 } 576 }); 577 } 578 if ((expansionStyle & TWISTIE) != 0) 579 toggle = new Twistie(this, SWT.NULL); 580 else if ((expansionStyle & TREE_NODE) != 0) 581 toggle = new TreeNode(this, SWT.NULL); 582 else 583 expanded = true; 584 if ((expansionStyle & EXPANDED) != 0) 585 expanded = true; 586 if (toggle != null) { 587 toggle.setExpanded(expanded); 588 toggle.addHyperlinkListener(new HyperlinkAdapter() { 589 @Override 590 public void linkActivated(HyperlinkEvent e) { 591 toggleState(); 592 } 593 }); 594 toggle.addPaintListener(e -> { 595 if (textLabel instanceof Label && !isFixedStyle()) 596 if (toggle.hover) { 597 textLabel.setForeground(toggle.getHoverDecorationColor()); 598 } else { 599 textLabel.setForeground(getTitleBarForeground()); 600 } 601 }); 602 toggle.addKeyListener(new KeyAdapter() { 603 @Override 604 public void keyPressed(KeyEvent e) { 605 if (e.keyCode == SWT.ARROW_UP) { 606 verticalMove(false); 607 e.doit = false; 608 } else if (e.keyCode == SWT.ARROW_DOWN) { 609 verticalMove(true); 610 e.doit = false; 611 } 612 } 613 }); 614 if ((getExpansionStyle()&FOCUS_TITLE)==0) { 615 toggle.paintFocus=false; 616 toggle.addFocusListener(new FocusListener() { 617 @Override 618 public void focusGained(FocusEvent e) { 619 if (textLabel != null) { 620 textLabel.redraw(); 621 } 622 } 623 624 @Override 625 public void focusLost(FocusEvent e) { 626 if (textLabel != null) { 627 textLabel.redraw(); 628 } 629 } 630 }); 631 } 632 } 633 if ((expansionStyle & FOCUS_TITLE) != 0) { 634 Hyperlink link = new Hyperlink(this, SWT.WRAP); 635 link.addHyperlinkListener(new HyperlinkAdapter() { 636 @Override 637 public void linkActivated(HyperlinkEvent e) { 638 programmaticToggleState(); 639 } 640 }); 641 textLabel = link; 642 } else if ((expansionStyle & NO_TITLE) == 0) { 643 final Label label = new Label(this, SWT.WRAP); 644 if (!isFixedStyle()) { 645 label.setCursor(FormsResources.getHandCursor()); 646 Listener listener = e -> { 647 switch (e.type) { 648 case SWT.MouseDown: 649 if (toggle != null) 650 toggle.setFocus(); 651 break; 652 case SWT.MouseUp: 653 label.setCursor(FormsResources.getBusyCursor()); 654 programmaticToggleState(); 655 label.setCursor(FormsResources.getHandCursor()); 656 break; 657 case SWT.MouseEnter: 658 if (toggle != null) { 659 label.setForeground(toggle.getHoverDecorationColor()); 660 toggle.hover = true; 661 toggle.redraw(); 662 } 663 break; 664 case SWT.MouseExit: 665 if (toggle != null) { 666 label.setForeground(getTitleBarForeground()); 667 toggle.hover = false; 668 toggle.redraw(); 669 } 670 break; 671 case SWT.Paint: 672 if (toggle != null && (getExpansionStyle() & NO_TITLE_FOCUS_BOX) == 0) { 673 paintTitleFocus(e.gc); 674 } 675 break; 676 } 677 }; 678 label.addListener(SWT.MouseDown, listener); 679 label.addListener(SWT.MouseUp, listener); 680 label.addListener(SWT.MouseEnter, listener); 681 label.addListener(SWT.MouseExit, listener); 682 label.addListener(SWT.Paint, listener); 683 } 684 textLabel = label; 685 } 686 if (textLabel != null) { 687 textLabel.setMenu(getMenu()); 688 textLabel.addTraverseListener(e -> { 689 if (e.detail == SWT.TRAVERSE_MNEMONIC) { 690 // steal the mnemonic 691 if (!isVisible() || !isEnabled()) 692 return; 693 if (FormUtil.mnemonicMatch(getText(), e.character)) { 694 e.doit = false; 695 if (!isFixedStyle()) { 696 programmaticToggleState(); 697 } 698 setFocus(); 699 } 700 } 701 }); 702 } 703 } 704 705 @Override forceFocus()706 public boolean forceFocus() { 707 return false; 708 } 709 710 /** 711 * Overrides 'super' to pass the menu to the text label. 712 * 713 * @param menu 714 * the menu from the parent to attach to this control. 715 */ 716 717 @Override setMenu(Menu menu)718 public void setMenu(Menu menu) { 719 if (textLabel != null) 720 textLabel.setMenu(menu); 721 super.setMenu(menu); 722 } 723 724 /** 725 * Prevents assignment of the layout manager - expandable composite uses its 726 * own layout. 727 */ 728 @Override setLayout(Layout layout)729 public final void setLayout(Layout layout) { 730 } 731 732 /** 733 * Sets the background of all the custom controls in the expandable. 734 */ 735 @Override setBackground(Color bg)736 public void setBackground(Color bg) { 737 super.setBackground(bg); 738 if ((getExpansionStyle() & TITLE_BAR) == 0) { 739 if (textLabel != null) 740 textLabel.setBackground(bg); 741 if (toggle != null) 742 toggle.setBackground(bg); 743 } 744 } 745 746 /** 747 * Sets the foreground of all the custom controls in the expandable. 748 */ 749 @Override setForeground(Color fg)750 public void setForeground(Color fg) { 751 super.setForeground(fg); 752 if (textLabel != null) 753 textLabel.setForeground(fg); 754 if (toggle != null) 755 toggle.setForeground(fg); 756 } 757 758 /** 759 * Sets the color of the toggle control. 760 * 761 * @param c 762 * the color object 763 */ setToggleColor(Color c)764 public void setToggleColor(Color c) { 765 if (toggle != null) 766 toggle.setDecorationColor(c); 767 } 768 769 /** 770 * Sets the active color of the toggle control (when the mouse enters the 771 * toggle area). 772 * 773 * @param c 774 * the active color object 775 */ setActiveToggleColor(Color c)776 public void setActiveToggleColor(Color c) { 777 if (toggle != null) 778 toggle.setHoverDecorationColor(c); 779 } 780 781 /** 782 * Sets the fonts of all the custom controls in the expandable. 783 */ 784 @Override setFont(Font font)785 public void setFont(Font font) { 786 super.setFont(font); 787 if (textLabel != null) 788 textLabel.setFont(font); 789 if (toggle != null) 790 toggle.setFont(font); 791 } 792 793 @Override setEnabled(boolean enabled)794 public void setEnabled(boolean enabled) { 795 if (textLabel != null) 796 textLabel.setEnabled(enabled); 797 if (toggle != null) 798 toggle.setEnabled(enabled); 799 super.setEnabled(enabled); 800 } 801 802 /** 803 * Sets the client of this expandable composite. The client must not be 804 * <samp>null </samp> and must be a direct child of this container. 805 * 806 * @param client 807 * the client that will be expanded or collapsed 808 */ setClient(Control client)809 public void setClient(Control client) { 810 Assert.isTrue(client != null && client.getParent().equals(this)); 811 this.client = client; 812 } 813 814 /** 815 * Returns the current expandable client. 816 * 817 * @return the client control 818 */ getClient()819 public Control getClient() { 820 return client; 821 } 822 823 /** 824 * Sets the title of the expandable composite. The title will act as a 825 * hyperlink and activating it will toggle the client between expanded and 826 * collapsed state. 827 * 828 * @param title 829 * the new title string 830 * @see #getText() 831 */ setText(String title)832 public void setText(String title) { 833 if (textLabel instanceof Label) { 834 ((Label) textLabel).setText(title); 835 } else if (textLabel instanceof Hyperlink) { 836 ((Hyperlink) textLabel).setText(title); 837 } else { 838 return; 839 } 840 layout(); 841 } 842 843 @Override setToolTipText(String string)844 public void setToolTipText(String string) { 845 super.setToolTipText(string); 846 // Also set on label, otherwise it's just on the background without text. 847 if (textLabel instanceof Label) { 848 ((Label) textLabel).setToolTipText(string); 849 } else if (textLabel instanceof Hyperlink) { 850 ((Hyperlink) textLabel).setToolTipText(string); 851 } 852 } 853 854 /** 855 * Returns the title string. 856 * 857 * @return the title string 858 * @see #setText(String) 859 */ getText()860 public String getText() { 861 if (textLabel instanceof Label) 862 return ((Label) textLabel).getText(); 863 else if (textLabel instanceof Hyperlink) 864 return ((Hyperlink) textLabel).getText(); 865 else 866 return ""; //$NON-NLS-1$ 867 } 868 869 /** 870 * Tests the expanded state of the composite. 871 * 872 * @return <samp>true </samp> if expanded, <samp>false </samp> if collapsed. 873 */ isExpanded()874 public boolean isExpanded() { 875 return expanded; 876 } 877 878 /** 879 * Returns the bitwise-ORed style bits for the expansion control. 880 * 881 * @return the bitwise-ORed style bits for the expansion control 882 */ getExpansionStyle()883 public int getExpansionStyle() { 884 return expansionStyle; 885 } 886 887 /** 888 * Programmatically changes expanded state. 889 * 890 * @param expanded 891 * the new expanded state 892 */ setExpanded(boolean expanded)893 public void setExpanded(boolean expanded) { 894 internalSetExpanded(expanded); 895 if (toggle != null) 896 toggle.setExpanded(expanded); 897 } 898 899 /** 900 * Performs the expansion state change for the expandable control. 901 * 902 * @param expanded 903 * the expansion state 904 */ internalSetExpanded(boolean expanded)905 protected void internalSetExpanded(boolean expanded) { 906 if (this.expanded != expanded) { 907 this.expanded = expanded; 908 if (getDescriptionControl() != null) 909 getDescriptionControl().setVisible(expanded); 910 if (client != null) 911 client.setVisible(expanded); 912 reflow(); 913 } 914 } 915 916 /** 917 * Adds the listener that will be notified when the expansion state changes. 918 * 919 * @param listener 920 * the listener to add 921 */ addExpansionListener(IExpansionListener listener)922 public void addExpansionListener(IExpansionListener listener) { 923 listeners.add(listener); 924 } 925 926 /** 927 * Removes the expansion listener. 928 * 929 * @param listener 930 * the listener to remove 931 */ removeExpansionListener(IExpansionListener listener)932 public void removeExpansionListener(IExpansionListener listener) { 933 listeners.remove(listener); 934 } 935 936 /** 937 * If TITLE_BAR or SHORT_TITLE_BAR style is used, title bar decoration will 938 * be painted behind the text in this method. The default implementation 939 * does nothing - subclasses are responsible for rendering the title area. 940 * 941 * @param e 942 * the paint event 943 */ onPaint(PaintEvent e)944 protected void onPaint(PaintEvent e) { 945 } 946 947 /** 948 * Returns description control that will be placed under the title if 949 * present. 950 * 951 * @return the description control or <samp>null </samp> if not used. 952 */ getDescriptionControl()953 protected Control getDescriptionControl() { 954 return null; 955 } 956 957 /** 958 * Returns the separator control that will be placed between the title and 959 * the description if present. 960 * 961 * @return the separator control or <samp>null </samp> if not used. 962 */ getSeparatorControl()963 protected Control getSeparatorControl() { 964 return null; 965 } 966 967 /** 968 * Computes the size of the expandable composite. 969 * 970 * @see org.eclipse.swt.widgets.Composite#computeSize 971 */ 972 @Override computeSize(int wHint, int hHint, boolean changed)973 public Point computeSize(int wHint, int hHint, boolean changed) { 974 checkWidget(); 975 Point size; 976 ExpandableLayout layout = (ExpandableLayout) getLayout(); 977 if (wHint == SWT.DEFAULT || hHint == SWT.DEFAULT) { 978 size = layout.computeSize(this, wHint, hHint, changed); 979 } else { 980 size = new Point(wHint, hHint); 981 } 982 Rectangle trim = computeTrim(0, 0, size.x, size.y); 983 return new Point(trim.width, trim.height); 984 } 985 986 /** 987 * Returns <samp>true </samp> if the composite is fixed i.e. cannot be 988 * expanded or collapsed. Fixed control will still contain the title, 989 * separator and description (if present) as well as the client, but will be 990 * in the permanent expanded state and the toggle affordance will not be 991 * shown. 992 * 993 * @return <samp>true </samp> if the control is fixed in the expanded state, 994 * <samp>false </samp> if it can be collapsed. 995 */ isFixedStyle()996 protected boolean isFixedStyle() { 997 return (expansionStyle & TWISTIE) == 0 998 && (expansionStyle & TREE_NODE) == 0; 999 } 1000 1001 /** 1002 * Returns the text client control. 1003 * 1004 * @return Returns the text client control if specified, or 1005 * <code>null</code> if not. 1006 */ getTextClient()1007 public Control getTextClient() { 1008 return textClient; 1009 } 1010 1011 /** 1012 * Sets the text client control. Text client is a control that is a child of 1013 * the expandable composite and is placed to the right of the text. It can 1014 * be used to place small image hyperlinks. If more than one control is 1015 * needed, use Composite to hold them. Care should be taken that the height 1016 * of the control is comparable to the height of the text. 1017 * 1018 * @param textClient 1019 * the textClient to set or <code>null</code> if not needed any 1020 * more. 1021 */ setTextClient(Control textClient)1022 public void setTextClient(Control textClient) { 1023 if (this.textClient != null) 1024 this.textClient.dispose(); 1025 this.textClient = textClient; 1026 } 1027 1028 /** 1029 * Returns the difference in height between the text and the text client (if 1030 * set). This difference can cause vertical alignment problems when two 1031 * expandable composites are placed side by side, one with and one without 1032 * the text client. Use this method obtain the value to add to either 1033 * <code>descriptionVerticalSpacing</code> (if you have description) or 1034 * <code>clientVerticalSpacing</code> to correct the alignment of the 1035 * expandable without the text client. 1036 * 1037 * @return the difference in height between the text and the text client or 1038 * 0 if no corrective action is needed. 1039 * @since 3.3 1040 */ getTextClientHeightDifference()1041 public int getTextClientHeightDifference() { 1042 if (textClient == null || textLabel == null) 1043 return 0; 1044 int theight = textLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT).y; 1045 int tcheight = textClient.computeSize(SWT.DEFAULT, SWT.DEFAULT).y; 1046 return Math.max(tcheight - theight, 0); 1047 } 1048 1049 /** 1050 * Tests if this expandable composite renders a title bar around the text. 1051 * 1052 * @return <code>true</code> for <code>TITLE_BAR</code> or 1053 * <code>SHORT_TITLE_BAR</code> styles, <code>false</code> 1054 * otherwise. 1055 */ hasTitleBar()1056 protected boolean hasTitleBar() { 1057 return (getExpansionStyle() & TITLE_BAR) != 0 1058 || (getExpansionStyle() & SHORT_TITLE_BAR) != 0; 1059 } 1060 1061 /** 1062 * Sets the color of the title bar foreground when TITLE_BAR style is used. 1063 * 1064 * @param color 1065 * the title bar foreground 1066 */ setTitleBarForeground(Color color)1067 public void setTitleBarForeground(Color color) { 1068 titleBarForeground = color; 1069 if (textLabel != null) 1070 textLabel.setForeground(color); 1071 } 1072 1073 /** 1074 * Returns the title bar foreground when TITLE_BAR style is used. 1075 * 1076 * @return the title bar foreground 1077 */ getTitleBarForeground()1078 public Color getTitleBarForeground() { 1079 return titleBarForeground; 1080 } 1081 1082 // end of APIs 1083 toggleState()1084 private void toggleState() { 1085 boolean newState = !isExpanded(); 1086 fireExpanding(newState, true); 1087 internalSetExpanded(newState); 1088 fireExpanding(newState, false); 1089 if (newState) 1090 FormUtil.ensureVisible(this); 1091 } 1092 fireExpanding(boolean state, boolean before)1093 private void fireExpanding(boolean state, boolean before) { 1094 int size = listeners.size(); 1095 if (size == 0) 1096 return; 1097 ExpansionEvent e = new ExpansionEvent(this, state); 1098 for (IExpansionListener listener : listeners) { 1099 if (before) 1100 listener.expansionStateChanging(e); 1101 else 1102 listener.expansionStateChanged(e); 1103 } 1104 } 1105 verticalMove(boolean down)1106 private void verticalMove(boolean down) { 1107 Composite parent = getParent(); 1108 Control[] children = parent.getChildren(); 1109 for (int i = 0; i < children.length; i++) { 1110 Control child = children[i]; 1111 if (child == this) { 1112 ExpandableComposite sibling = getSibling(children, i, down); 1113 if (sibling != null && sibling.toggle != null) { 1114 sibling.setFocus(); 1115 } 1116 break; 1117 } 1118 } 1119 } 1120 getSibling(Control[] children, int index, boolean down)1121 private ExpandableComposite getSibling(Control[] children, int index, 1122 boolean down) { 1123 int loc = down ? index + 1 : index - 1; 1124 while (loc >= 0 && loc < children.length) { 1125 Control c = children[loc]; 1126 if (c instanceof ExpandableComposite && c.isVisible()) 1127 return (ExpandableComposite) c; 1128 loc = down ? loc + 1 : loc - 1; 1129 } 1130 return null; 1131 } 1132 programmaticToggleState()1133 private void programmaticToggleState() { 1134 if (toggle != null) 1135 toggle.setExpanded(!toggle.isExpanded()); 1136 toggleState(); 1137 } 1138 paintTitleFocus(GC gc)1139 private void paintTitleFocus(GC gc) { 1140 Point size = textLabel.getSize(); 1141 gc.setBackground(textLabel.getBackground()); 1142 gc.setForeground(textLabel.getForeground()); 1143 if (toggle.isFocusControl()) 1144 gc.drawFocus(0, 0, size.x, size.y); 1145 } 1146 reflow()1147 void reflow() { 1148 Composite c = this; 1149 while (c != null) { 1150 c.setRedraw(false); 1151 c = c.getParent(); 1152 if (c instanceof SharedScrolledComposite || c instanceof Shell) { 1153 break; 1154 } 1155 } 1156 c = this; 1157 while (c != null) { 1158 c.requestLayout(); 1159 c = c.getParent(); 1160 if (c instanceof SharedScrolledComposite) { 1161 ((SharedScrolledComposite) c).reflow(true); 1162 break; 1163 } 1164 } 1165 c = this; 1166 while (c != null) { 1167 c.setRedraw(true); 1168 c = c.getParent(); 1169 if (c instanceof SharedScrolledComposite || c instanceof Shell) { 1170 break; 1171 } 1172 } 1173 } 1174 } 1175