From 657d80bf5dd480db3d52642f620d006f6b2b86aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Tue, 28 Apr 2026 10:09:19 +0200 Subject: [PATCH] Add TriStateSwitchButton with two active states and off Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.classpath | 2 +- .../.settings/org.eclipse.jdt.core.prefs | 6 +- .../META-INF/MANIFEST.MF | 2 +- .../snippets/TriStateSwitchButtonSnippet.java | 192 ++++ .../widgets/opal/switchbutton/TriState.java | 36 + .../switchbutton/TriStateSwitchButton.java | 972 ++++++++++++++++++ 6 files changed, 1205 insertions(+), 5 deletions(-) create mode 100644 widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/src/org/eclipse/nebula/widgets/opal/switchbutton/snippets/TriStateSwitchButtonSnippet.java create mode 100644 widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton/src/org/eclipse/nebula/widgets/opal/switchbutton/TriState.java create mode 100644 widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton/src/org/eclipse/nebula/widgets/opal/switchbutton/TriStateSwitchButton.java diff --git a/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/.classpath b/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/.classpath index 81fe078c20..375961e4d6 100644 --- a/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/.classpath +++ b/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/.classpath @@ -1,6 +1,6 @@ - + diff --git a/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/.settings/org.eclipse.jdt.core.prefs b/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/.settings/org.eclipse.jdt.core.prefs index d4540a53f9..3a79233b13 100644 --- a/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/.settings/org.eclipse.jdt.core.prefs +++ b/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/.settings/org.eclipse.jdt.core.prefs @@ -1,10 +1,10 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 -org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 +org.eclipse.jdt.core.compiler.compliance=21 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning org.eclipse.jdt.core.compiler.release=enabled -org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.compiler.source=21 diff --git a/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/META-INF/MANIFEST.MF b/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/META-INF/MANIFEST.MF index 021f8f4f70..36e443046b 100644 --- a/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/META-INF/MANIFEST.MF +++ b/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/META-INF/MANIFEST.MF @@ -4,7 +4,7 @@ Bundle-Name: Nebula Opal Switch Button Snippets Bundle-SymbolicName: org.eclipse.nebula.widgets.opal.switchbutton.snippets Bundle-Version: 1.0.0.qualifier Bundle-Vendor: Eclipse Nebula -Bundle-RequiredExecutionEnvironment: JavaSE-17 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Require-Bundle: org.eclipse.nebula.widgets.opal.switchbutton;bundle-version="1.0.0", org.eclipse.swt Automatic-Module-Name: org.eclipse.nebula.widgets.opal.switchbutton.snippets diff --git a/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/src/org/eclipse/nebula/widgets/opal/switchbutton/snippets/TriStateSwitchButtonSnippet.java b/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/src/org/eclipse/nebula/widgets/opal/switchbutton/snippets/TriStateSwitchButtonSnippet.java new file mode 100644 index 0000000000..d59929696f --- /dev/null +++ b/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton.snippets/src/org/eclipse/nebula/widgets/opal/switchbutton/snippets/TriStateSwitchButtonSnippet.java @@ -0,0 +1,192 @@ +/******************************************************************************* + * Copyright (c) 2026 Christoph Läubrich + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.nebula.widgets.opal.switchbutton.snippets; + +import org.eclipse.nebula.widgets.opal.switchbutton.TriState; +import org.eclipse.nebula.widgets.opal.switchbutton.TriStateSwitchButton; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; + +/** + * A simple snippet for the TriStateSwitchButton widget. + * + *

+ * Click behavior: + *

    + *
  • When FIRST or SECOND: any click → OFF
  • + *
  • When OFF: click left section → FIRST, click right section → SECOND, click + * center section → no change
  • + *
+ *

+ */ +public class TriStateSwitchButtonSnippet { + + public static void main(final String[] args) { + final Display display = new Display(); + final Shell shell = new Shell(display); + shell.setText("TriStateSwitchButton Snippet"); + shell.setSize(700, 700); + shell.setLayout(new GridLayout(1, false)); + + // Default – starts in OFF state + final TriStateSwitchButton button1 = new TriStateSwitchButton(shell, SWT.NONE); + button1.setText("Default (starts OFF)"); + + // Custom section labels + final TriStateSwitchButton button2 = new TriStateSwitchButton(shell, SWT.NONE); + button2.setTextForFirst("Left"); + button2.setTextForOff("Neutral"); + button2.setTextForSecond("Right"); + button2.setText("Custom labels"); + + // Starts in FIRST state + final TriStateSwitchButton button3First = new TriStateSwitchButton(shell, SWT.NONE); + button3First.setState(TriState.FIRST); + button3First.setText("Starts in FIRST state"); + + // Starts in SECOND state + final TriStateSwitchButton button3Second = new TriStateSwitchButton(shell, SWT.NONE); + button3Second.setState(TriState.SECOND); + button3Second.setText("Starts in SECOND state"); + + // With a border + final TriStateSwitchButton button4 = new TriStateSwitchButton(shell, SWT.NONE); + button4.setBorderColor(display.getSystemColor(SWT.COLOR_DARK_RED)); + button4.setText("With border"); + + // Disabled – all three starting states + final TriStateSwitchButton button5Off = new TriStateSwitchButton(shell, SWT.NONE); + button5Off.setEnabled(false); + button5Off.setText("Disabled (OFF)"); + + final TriStateSwitchButton button5First = new TriStateSwitchButton(shell, SWT.NONE); + button5First.setState(TriState.FIRST); + button5First.setEnabled(false); + button5First.setText("Disabled (FIRST)"); + + final TriStateSwitchButton button5Second = new TriStateSwitchButton(shell, SWT.NONE); + button5Second.setState(TriState.SECOND); + button5Second.setEnabled(false); + button5Second.setText("Disabled (SECOND)"); + + // Without glow / focus effect + final TriStateSwitchButton button6 = new TriStateSwitchButton(shell, SWT.NONE); + button6.setFocusColor(null); + button6.setText("No focus/hover effect"); + + // Square (non-rounded) + final TriStateSwitchButton button7 = new TriStateSwitchButton(shell, SWT.NONE); + button7.setRound(false); + button7.setText("Square style"); + + // Custom colors for all three sections + final TriStateSwitchButton button8 = new TriStateSwitchButton(shell, SWT.NONE); + button8.setFirstBackgroundColor(display.getSystemColor(SWT.COLOR_DARK_BLUE)); + button8.setFirstForegroundColor(display.getSystemColor(SWT.COLOR_WHITE)); + button8.setOffBackgroundColor(display.getSystemColor(SWT.COLOR_DARK_GRAY)); + button8.setOffForegroundColor(display.getSystemColor(SWT.COLOR_WHITE)); + button8.setSecondBackgroundColor(display.getSystemColor(SWT.COLOR_DARK_RED)); + button8.setSecondForegroundColor(display.getSystemColor(SWT.COLOR_WHITE)); + button8.setButtonBorderColor(display.getSystemColor(SWT.COLOR_BLACK)); + button8.setText("Custom section colors"); + + // Selection listener – reports the new state + final TriStateSwitchButton button9 = new TriStateSwitchButton(shell, SWT.NONE); + button9.setText("Selection listener"); + button9.addSelectionListener(new SelectionListener() { + + @Override + public void widgetSelected(final SelectionEvent e) { + System.out.println("New state: " + button9.getState()); + } + + @Override + public void widgetDefaultSelected(final SelectionEvent e) { + } + }); + + // Selection listener with doit=false – cancels the state change + final TriStateSwitchButton button10 = new TriStateSwitchButton(shell, SWT.NONE); + button10.setText("Listener with doit=false (change blocked)"); + button10.addSelectionListener(new SelectionListener() { + + @Override + public void widgetSelected(final SelectionEvent e) { + System.out.println("Change vetoed – state stays: " + button10.getState()); + e.doit = false; + } + + @Override + public void widgetDefaultSelected(final SelectionEvent e) { + } + }); + + // Low-level SWT listener + final TriStateSwitchButton button11 = new TriStateSwitchButton(shell, SWT.NONE); + button11.setFocusColor(null); + button11.setText("Low-level SWT listener (beeps on change)"); + button11.addListener(SWT.Selection, new Listener() { + + @Override + public void handleEvent(final Event event) { + event.display.beep(); + } + }); + + // Custom font + final TriStateSwitchButton button12 = new TriStateSwitchButton(shell, SWT.NONE); + final Font font = new Font(display, "Courier New", 16, SWT.BOLD | SWT.ITALIC); + shell.addDisposeListener(new DisposeListener() { + + @Override + public void widgetDisposed(final DisposeEvent e) { + font.dispose(); + } + }); + button12.setFont(font); + button12.setText("Custom font"); + + // Custom margins and arc + final TriStateSwitchButton button13 = new TriStateSwitchButton(shell, SWT.NONE); + button13.setInsideMargin(10, 3); + button13.setArc(4); + button13.setText("Custom margins & arc"); + + // Widget background and foreground (label area) + final TriStateSwitchButton button14 = new TriStateSwitchButton(shell, SWT.NONE); + button14.setBackground(display.getSystemColor(SWT.COLOR_YELLOW)); + button14.setForeground(display.getSystemColor(SWT.COLOR_DARK_RED)); + button14.setText("Custom widget background/foreground"); + + shell.pack(); + shell.open(); + + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + display.dispose(); + } + +} diff --git a/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton/src/org/eclipse/nebula/widgets/opal/switchbutton/TriState.java b/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton/src/org/eclipse/nebula/widgets/opal/switchbutton/TriState.java new file mode 100644 index 0000000000..3c72c73427 --- /dev/null +++ b/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton/src/org/eclipse/nebula/widgets/opal/switchbutton/TriState.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2026 Christoph Läubrich + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.nebula.widgets.opal.switchbutton; + +/** + * Represents the three possible states of a {@link TriStateSwitchButton}. + * + *
    + *
  • {@link #FIRST} - the first active state, shown at the left position.
  • + *
  • {@link #OFF} - the neutral/off state, shown at the center position.
  • + *
  • {@link #SECOND} - the second active state, shown at the right position.
  • + *
+ */ +public enum TriState { + + /** The first active state (left position). */ + FIRST, + + /** The neutral/off state (center position). */ + OFF, + + /** The second active state (right position). */ + SECOND + +} diff --git a/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton/src/org/eclipse/nebula/widgets/opal/switchbutton/TriStateSwitchButton.java b/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton/src/org/eclipse/nebula/widgets/opal/switchbutton/TriStateSwitchButton.java new file mode 100644 index 0000000000..245b719a92 --- /dev/null +++ b/widgets/opal/switchbutton/org.eclipse.nebula.widgets.opal.switchbutton/src/org/eclipse/nebula/widgets/opal/switchbutton/TriStateSwitchButton.java @@ -0,0 +1,972 @@ +/******************************************************************************* + * Copyright (c) 2026 Christoph Läubrich + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.nebula.widgets.opal.switchbutton; + +import org.eclipse.nebula.widgets.opal.commons.SWTGraphicUtil; +import org.eclipse.nebula.widgets.opal.commons.SelectionListenerUtil; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseTrackListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; + +/** + * Instances of this class are tri-state switch buttons with two active states + * and one off state. + * + *

+ * The button has three equal sections: + *

    + *
  1. Left (FIRST) – the first active state.
  2. + *
  3. Center (OFF) – the neutral/off state.
  4. + *
  5. Right (SECOND) – the second active state.
  6. + *
+ * A sliding toggle knob indicates which state is currently active. + *

+ * + *

+ * Click behavior: + *

    + *
  • When the button is in {@link TriState#FIRST} or {@link TriState#SECOND}, + * any click switches it to {@link TriState#OFF}.
  • + *
  • When the button is in {@link TriState#OFF}, clicking the left section + * activates {@link TriState#FIRST} and clicking the right section activates + * {@link TriState#SECOND}. Clicking the center section has no effect.
  • + *
+ *

+ * + *
+ *
Styles:
+ *
(none)
+ *
Events:
+ *
Selection
+ *
+ */ +public class TriStateSwitchButton extends Canvas { + + /** Current tri-state selection. */ + private TriState state; + + /** Text displayed inside the first (left) section. Default: "A". */ + private String textForFirst; + + /** Text displayed inside the off (center) section. Default: "Off". */ + private String textForOff; + + /** Text displayed inside the second (right) section. Default: "B". */ + private String textForSecond; + + /** Label text displayed beside the button. Default: "". */ + private String text; + + /** If true, use round rectangles instead of sharp rectangles. Default: true. */ + private boolean round; + + /** If not null, draw a border around the whole widget. Default: null. */ + private Color borderColor; + + /** If not null, draw a glow ring around the toggle when hovered. Default: dark grey. */ + private Color focusColor; + + /** Foreground/background colors for the first (left, FIRST state) section. */ + private Color firstForegroundColor, firstBackgroundColor; + + /** Foreground/background colors for the off (center, OFF state) section. */ + private Color offForegroundColor, offBackgroundColor; + + /** Foreground/background colors for the second (right, SECOND state) section. */ + private Color secondForegroundColor, secondBackgroundColor; + + /** Colors for the sliding toggle knob. */ + private Color buttonBorderColor, buttonBackgroundColor1, buttonBackgroundColor2; + + /** Gap between the button body and the label text. Default: 5. */ + private int gap = 5; + + /** Horizontal padding inside each section. Default: 5. */ + private int insideMarginX = 5; + + /** Vertical padding inside the button. Default: 5. */ + private int insideMarginY = 5; + + /** Arc radius for rounded rectangles. Default: 3. */ + private int arc = 3; + + /** Current paint GC, set during {@link #onPaint(PaintEvent)}. */ + private GC gc; + + /** True when the mouse is inside the widget. */ + private boolean mouseInside; + + /** + * Cached button body size from the most recent paint, used for click + * hit-testing. May be null before the first paint. + */ + private Point lastButtonSize; + + /** + * Constructs a new tri-state switch button. + * + * @param parent the parent composite (cannot be null) + * @param style the SWT style bits + * + * @exception IllegalArgumentException if parent is null + * @exception SWTException if called from a wrong thread + */ + public TriStateSwitchButton(final Composite parent, final int style) { + super(parent, style | SWT.DOUBLE_BUFFERED); + + state = TriState.OFF; + text = ""; + textForFirst = "A"; + textForOff = "Off"; + textForSecond = "B"; + round = true; + borderColor = null; + focusColor = getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); + + firstForegroundColor = getDisplay().getSystemColor(SWT.COLOR_WHITE); + firstBackgroundColor = SWTGraphicUtil.getDefaultColor(this, 0, 112, 195); + + offForegroundColor = getDisplay().getSystemColor(SWT.COLOR_BLACK); + offBackgroundColor = SWTGraphicUtil.getDefaultColor(this, 203, 203, 203); + + secondForegroundColor = getDisplay().getSystemColor(SWT.COLOR_WHITE); + secondBackgroundColor = SWTGraphicUtil.getDefaultColor(this, 0, 150, 50); + + buttonBorderColor = SWTGraphicUtil.getDefaultColor(this, 96, 96, 96); + buttonBackgroundColor1 = SWTGraphicUtil.getDefaultColor(this, 254, 254, 254); + buttonBackgroundColor2 = SWTGraphicUtil.getDefaultColor(this, 192, 192, 192); + + addPaintListener(this::onPaint); + + addListener(SWT.MouseUp, e -> { + final TriState previousState = state; + final TriState newState = computeNewState(e.x); + if (newState == previousState) { + return; + } + state = newState; + if (SelectionListenerUtil.fireSelectionListeners(this, e)) { + redraw(); + } else { + state = previousState; + } + }); + + mouseInside = false; + addMouseTrackListener(new MouseTrackListener() { + + @Override + public void mouseHover(final MouseEvent e) { + mouseInside = true; + redraw(); + } + + @Override + public void mouseExit(final MouseEvent e) { + mouseInside = false; + redraw(); + } + + @Override + public void mouseEnter(final MouseEvent e) { + mouseInside = true; + redraw(); + } + }); + } + + /** + * Computes the new state based on the current state and the click X coordinate. + * + * @param clickX the X coordinate of the mouse-up event + * @return the new state; equals the current state if no transition should occur + */ + private TriState computeNewState(final int clickX) { + if (state != TriState.OFF) { + // Any click when active → switch to OFF + return TriState.OFF; + } + // Currently OFF: determine which section was clicked + if (lastButtonSize == null) { + return TriState.OFF; + } + final int sectionWidth = lastButtonSize.x / 3; + // Button body spans x=[2, 2+buttonSize.x) + if (clickX < 2 || clickX >= 2 + lastButtonSize.x) { + return TriState.OFF; + } + final int relX = clickX - 2; + if (relX < sectionWidth) { + return TriState.FIRST; + } + if (relX >= 2 * sectionWidth) { + return TriState.SECOND; + } + // Center section click while OFF → no change + return TriState.OFF; + } + + // ------------------------------------------------------------------------- + // Painting + // ------------------------------------------------------------------------- + + private void onPaint(final PaintEvent event) { + final Rectangle rect = getClientArea(); + if (rect.width == 0 || rect.height == 0) { + return; + } + gc = event.gc; + gc.setAntialias(SWT.ON); + + final Point buttonSize = computeButtonSize(); + lastButtonSize = buttonSize; + drawSwitchButton(buttonSize); + drawText(buttonSize); + + if (borderColor != null) { + drawBorder(); + } + } + + /** + * Draws the three-section button body and the sliding toggle knob. + */ + private void drawSwitchButton(final Point buttonSize) { + gc.setForeground(buttonBorderColor); + if (round) { + gc.drawRoundRectangle(2, 2, buttonSize.x, buttonSize.y, arc, arc); + } else { + gc.drawRectangle(2, 2, buttonSize.x, buttonSize.y); + } + + final boolean enabled = isEnabled(); + drawSection(buttonSize, 0, textForFirst, firstForegroundColor, firstBackgroundColor, enabled); + drawSection(buttonSize, 1, textForOff, offForegroundColor, offBackgroundColor, enabled); + drawSection(buttonSize, 2, textForSecond, secondForegroundColor, secondBackgroundColor, enabled); + + gc.setClipping(getClientArea()); + drawToggleButton(buttonSize); + } + + /** + * Draws one of the three sections of the button. + * + * @param buttonSize total size of the button body + * @param sectionIndex 0 = left (FIRST), 1 = center (OFF), 2 = right (SECOND) + * @param label the text label for this section + * @param fgColor foreground (text) color when enabled + * @param bgColor background color when enabled + * @param enabled whether the widget is enabled + */ + private void drawSection(final Point buttonSize, final int sectionIndex, final String label, + final Color fgColor, final Color bgColor, final boolean enabled) { + final int sectionWidth = buttonSize.x / 3; + final int sectionX = sectionIndex * sectionWidth; + + final Color bg; + final Color fg; + if (enabled) { + bg = bgColor; + fg = fgColor; + } else { + bg = gc.getDevice().getSystemColor(SWT.COLOR_TEXT_DISABLED_BACKGROUND); + fg = gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_DISABLED_FOREGROUND); + } + + gc.setForeground(bg); + gc.setBackground(bg); + gc.setClipping(sectionX + 3, 3, sectionWidth, buttonSize.y - 1); + if (round) { + gc.fillRoundRectangle(2, 2, buttonSize.x, buttonSize.y, arc, arc); + } else { + gc.fillRectangle(2, 2, buttonSize.x, buttonSize.y); + } + + gc.setForeground(fg); + final Point textSize = gc.textExtent(label); + gc.drawString(label, sectionX + (sectionWidth - textSize.x) / 2 + arc, + (buttonSize.y - textSize.y) / 2 + arc); + } + + /** + * Draws the gradient toggle knob over the currently active section. + */ + private void drawToggleButton(final Point buttonSize) { + final int sectionWidth = buttonSize.x / 3; + final int knobX; + if (state == TriState.FIRST) { + knobX = 2; + } else if (state == TriState.SECOND) { + knobX = 2 * sectionWidth + 2; + } else { + knobX = sectionWidth + 2; + } + + gc.setForeground(buttonBackgroundColor1); + gc.setBackground(buttonBackgroundColor2); + gc.fillGradientRectangle(knobX, arc, sectionWidth, buttonSize.y - 1, true); + + gc.setForeground(buttonBorderColor); + gc.drawRoundRectangle(knobX, 2, sectionWidth, buttonSize.y, arc, arc); + + if (focusColor != null && mouseInside) { + gc.setForeground(focusColor); + gc.setLineWidth(2); + gc.drawRoundRectangle(knobX + 1, 3, sectionWidth - 2, buttonSize.y - 2, 3, 3); + gc.setLineWidth(1); + } + } + + /** + * Computes the size of the button body (excluding the label text). + * Each section is equally wide: {@code sectionWidth = maxTextWidth + 2 * insideMarginX}. + */ + private Point computeButtonSize() { + final Point sizeFirst = gc.stringExtent(textForFirst); + final Point sizeOff = gc.stringExtent(textForOff); + final Point sizeSecond = gc.stringExtent(textForSecond); + + final int maxWidth = Math.max(Math.max(sizeFirst.x, sizeOff.x), sizeSecond.x); + final int maxHeight = Math.max(Math.max(sizeFirst.y, sizeOff.y), sizeSecond.y); + + final int sectionWidth = maxWidth + 2 * insideMarginX; + final int width = sectionWidth * 3; + final int height = maxHeight + 2 * insideMarginY; + + return new Point(width, height); + } + + /** + * Draws the optional label text to the right of the button body. + */ + private void drawText(final Point buttonSize) { + gc.setForeground(getForeground()); + gc.setBackground(getBackground()); + + final int widgetHeight = this.computeSize(0, 0, true).y; + final int textHeight = gc.stringExtent(text).y; + final int x = 2 + buttonSize.x + gap; + + gc.drawString(text, x, (widgetHeight - textHeight) / 2); + } + + /** + * Draws the optional border around the entire widget. + */ + private void drawBorder() { + if (borderColor == null) { + return; + } + gc.setForeground(borderColor); + final Point temp = this.computeSize(0, 0, false); + if (round) { + gc.drawRoundRectangle(0, 0, temp.x - 2, temp.y - 2, 3, 3); + } else { + gc.drawRectangle(0, 0, temp.x - 2, temp.y - 2); + } + } + + // ------------------------------------------------------------------------- + // Selection listeners + // ------------------------------------------------------------------------- + + /** + * Adds a selection listener that is notified when the state changes. + * + * @param listener the listener to add (cannot be null) + * + * @exception IllegalArgumentException if listener is null + * @exception SWTException if the widget is disposed or called from a wrong thread + * + * @see SelectionListener + * @see #removeSelectionListener + * @see SelectionEvent + */ + public void addSelectionListener(final SelectionListener listener) { + addTypedListener(listener, SWT.Selection); + } + + /** + * Removes a previously added selection listener. + * + * @param listener the listener to remove (cannot be null) + * + * @exception IllegalArgumentException if listener is null + * @exception SWTException if the widget is disposed or called from a wrong thread + * + * @see SelectionListener + * @see #addSelectionListener + */ + public void removeSelectionListener(final SelectionListener listener) { + removeTypedListener(SWT.Selection, listener); + } + + // ------------------------------------------------------------------------- + // Size + // ------------------------------------------------------------------------- + + /** @see org.eclipse.swt.widgets.Composite#computeSize(int, int, boolean) */ + @Override + public Point computeSize(final int wHint, final int hHint, final boolean changed) { + checkWidget(); + boolean disposeGC = false; + if (gc == null || gc.isDisposed()) { + gc = new GC(this); + disposeGC = true; + } + + final Point buttonSize = computeButtonSize(); + int width = buttonSize.x; + int height = buttonSize.y; + + if (text != null && text.trim().length() > 0) { + final Point textSize = gc.textExtent(text); + width += textSize.x + gap + 1; + } + + width += 4; + height += 6; + + if (disposeGC) { + gc.dispose(); + } + + return new Point(width, height); + } + + // ------------------------------------------------------------------------- + // State + // ------------------------------------------------------------------------- + + /** + * Returns the current tri-state of the button. + * + * @return the current {@link TriState} + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public TriState getState() { + checkWidget(); + return state; + } + + /** + * Sets the current tri-state of the button and redraws it. + * + * @param state the new state (cannot be null) + * + * @exception IllegalArgumentException if state is null + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setState(final TriState state) { + checkWidget(); + if (state == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + this.state = state; + redraw(); + } + + // ------------------------------------------------------------------------- + // Text properties + // ------------------------------------------------------------------------- + + /** + * Returns the label text of the first (left) section. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public String getTextForFirst() { + checkWidget(); + return textForFirst; + } + + /** + * Sets the label text of the first (left) section. + * + * @param textForFirst the new text (cannot be null) + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setTextForFirst(final String textForFirst) { + checkWidget(); + this.textForFirst = textForFirst; + redraw(); + } + + /** + * Returns the label text of the off (center) section. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public String getTextForOff() { + checkWidget(); + return textForOff; + } + + /** + * Sets the label text of the off (center) section. + * + * @param textForOff the new text (cannot be null) + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setTextForOff(final String textForOff) { + checkWidget(); + this.textForOff = textForOff; + redraw(); + } + + /** + * Returns the label text of the second (right) section. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public String getTextForSecond() { + checkWidget(); + return textForSecond; + } + + /** + * Sets the label text of the second (right) section. + * + * @param textForSecond the new text (cannot be null) + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setTextForSecond(final String textForSecond) { + checkWidget(); + this.textForSecond = textForSecond; + redraw(); + } + + /** + * Returns the label text displayed beside the button. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public String getText() { + checkWidget(); + return text; + } + + /** + * Sets the label text displayed beside the button. + * + * @param text the new text (cannot be null) + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setText(final String text) { + checkWidget(); + this.text = text; + redraw(); + } + + // ------------------------------------------------------------------------- + // Visual style + // ------------------------------------------------------------------------- + + /** + * Returns whether the widget uses round rectangles. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public boolean isRound() { + checkWidget(); + return round; + } + + /** + * Sets whether the widget uses round rectangles. + * + * @param round if true, round rectangles are used; otherwise sharp rectangles + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setRound(final boolean round) { + checkWidget(); + this.round = round; + redraw(); + } + + /** + * Returns the border color. Null means no border is drawn. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public Color getBorderColor() { + checkWidget(); + return borderColor; + } + + /** + * Sets the border color. Pass null to suppress the border. + * + * @param borderColor the new border color, or null + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setBorderColor(final Color borderColor) { + checkWidget(); + this.borderColor = borderColor; + redraw(); + } + + /** + * Returns the focus (hover glow) color. Null means no glow effect. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public Color getFocusColor() { + checkWidget(); + return focusColor; + } + + /** + * Sets the focus (hover glow) color. Pass null to suppress the glow. + * + * @param focusColor the new focus color, or null + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setFocusColor(final Color focusColor) { + checkWidget(); + this.focusColor = focusColor; + redraw(); + } + + // ------------------------------------------------------------------------- + // Section colors – FIRST state + // ------------------------------------------------------------------------- + + /** + * Returns the foreground color of the first (left, FIRST) section. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public Color getFirstForegroundColor() { + checkWidget(); + return firstForegroundColor; + } + + /** + * Sets the foreground color of the first (left, FIRST) section. + * + * @param firstForegroundColor the new color + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setFirstForegroundColor(final Color firstForegroundColor) { + checkWidget(); + this.firstForegroundColor = firstForegroundColor; + redraw(); + } + + /** + * Returns the background color of the first (left, FIRST) section. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public Color getFirstBackgroundColor() { + checkWidget(); + return firstBackgroundColor; + } + + /** + * Sets the background color of the first (left, FIRST) section. + * + * @param firstBackgroundColor the new color + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setFirstBackgroundColor(final Color firstBackgroundColor) { + checkWidget(); + this.firstBackgroundColor = firstBackgroundColor; + redraw(); + } + + // ------------------------------------------------------------------------- + // Section colors – OFF state + // ------------------------------------------------------------------------- + + /** + * Returns the foreground color of the off (center, OFF) section. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public Color getOffForegroundColor() { + checkWidget(); + return offForegroundColor; + } + + /** + * Sets the foreground color of the off (center, OFF) section. + * + * @param offForegroundColor the new color + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setOffForegroundColor(final Color offForegroundColor) { + checkWidget(); + this.offForegroundColor = offForegroundColor; + redraw(); + } + + /** + * Returns the background color of the off (center, OFF) section. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public Color getOffBackgroundColor() { + checkWidget(); + return offBackgroundColor; + } + + /** + * Sets the background color of the off (center, OFF) section. + * + * @param offBackgroundColor the new color + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setOffBackgroundColor(final Color offBackgroundColor) { + checkWidget(); + this.offBackgroundColor = offBackgroundColor; + redraw(); + } + + // ------------------------------------------------------------------------- + // Section colors – SECOND state + // ------------------------------------------------------------------------- + + /** + * Returns the foreground color of the second (right, SECOND) section. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public Color getSecondForegroundColor() { + checkWidget(); + return secondForegroundColor; + } + + /** + * Sets the foreground color of the second (right, SECOND) section. + * + * @param secondForegroundColor the new color + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setSecondForegroundColor(final Color secondForegroundColor) { + checkWidget(); + this.secondForegroundColor = secondForegroundColor; + redraw(); + } + + /** + * Returns the background color of the second (right, SECOND) section. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public Color getSecondBackgroundColor() { + checkWidget(); + return secondBackgroundColor; + } + + /** + * Sets the background color of the second (right, SECOND) section. + * + * @param secondBackgroundColor the new color + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setSecondBackgroundColor(final Color secondBackgroundColor) { + checkWidget(); + this.secondBackgroundColor = secondBackgroundColor; + redraw(); + } + + // ------------------------------------------------------------------------- + // Toggle knob colors + // ------------------------------------------------------------------------- + + /** + * Returns the border color of the toggle knob. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public Color getButtonBorderColor() { + checkWidget(); + return buttonBorderColor; + } + + /** + * Sets the border color of the toggle knob. + * + * @param buttonBorderColor the new color + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setButtonBorderColor(final Color buttonBorderColor) { + checkWidget(); + this.buttonBorderColor = buttonBorderColor; + redraw(); + } + + /** + * Returns the first (top) gradient color of the toggle knob. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public Color getButtonBackgroundColor1() { + checkWidget(); + return buttonBackgroundColor1; + } + + /** + * Sets the first (top) gradient color of the toggle knob. + * + * @param buttonBackgroundColor1 the new color + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setButtonBackgroundColor1(final Color buttonBackgroundColor1) { + checkWidget(); + this.buttonBackgroundColor1 = buttonBackgroundColor1; + redraw(); + } + + /** + * Returns the second (bottom) gradient color of the toggle knob. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public Color getButtonBackgroundColor2() { + checkWidget(); + return buttonBackgroundColor2; + } + + /** + * Sets the second (bottom) gradient color of the toggle knob. + * + * @param buttonBackgroundColor2 the new color + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setButtonBackgroundColor2(final Color buttonBackgroundColor2) { + checkWidget(); + this.buttonBackgroundColor2 = buttonBackgroundColor2; + redraw(); + } + + // ------------------------------------------------------------------------- + // Layout metrics + // ------------------------------------------------------------------------- + + /** + * Returns the gap between the button body and the label text. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public int getGap() { + checkWidget(); + return gap; + } + + /** + * Sets the gap between the button body and the label text. + * + * @param gap the new gap value in pixels + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setGap(final int gap) { + checkWidget(); + this.gap = gap; + redraw(); + } + + /** + * Returns the horizontal and vertical padding inside each section as a Point. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public Point getInsideMargin() { + checkWidget(); + return new Point(insideMarginX, insideMarginY); + } + + /** + * Sets the horizontal and vertical padding inside each section. + * + * @param insideMarginX horizontal margin in pixels + * @param insideMarginY vertical margin in pixels + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setInsideMargin(final int insideMarginX, final int insideMarginY) { + checkWidget(); + this.insideMarginX = insideMarginX; + this.insideMarginY = insideMarginY; + redraw(); + } + + /** + * Sets the horizontal and vertical padding inside each section. + * + * @param insideMargin the new margin (x=horizontal, y=vertical) + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setInsideMargin(final Point insideMargin) { + checkWidget(); + insideMarginX = insideMargin.x; + insideMarginY = insideMargin.y; + redraw(); + } + + /** + * Returns the arc radius used for rounded rectangles. + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public int getArc() { + checkWidget(); + return arc; + } + + /** + * Sets the arc radius used for rounded rectangles. + * + * @param arc the new arc radius in pixels + * + * @exception SWTException if the widget is disposed or called from a wrong thread + */ + public void setArc(final int arc) { + checkWidget(); + this.arc = arc; + redraw(); + } + +}