diff --git a/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabFolder.java b/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabFolder.java index 98efb4610a..a6276b6db8 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabFolder.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabFolder.java @@ -200,6 +200,7 @@ public class CTabFolder extends Composite { // close, min/max and chevron buttons boolean showClose = false; boolean showUnselectedClose = true; + boolean dirtyIndicatorStyle = false; boolean showMin = false; boolean minimized = false; @@ -2794,7 +2795,7 @@ boolean setItemLocation(GC gc) { item.x = leftItemEdge; item.y = y; item.showing = true; - if (showClose || item.showClose) { + if (showClose || item.showClose || (dirtyIndicatorStyle && item.showDirty)) { item.closeRect.x = leftItemEdge - renderer.computeTrim(i, SWT.NONE, 0, 0, 0, 0).x; item.closeRect.y = onBottom ? size.y - borderBottom - tabHeight + (tabHeight - closeButtonSize.y)/2: borderTop + (tabHeight - closeButtonSize.y)/2; } @@ -2896,7 +2897,7 @@ boolean setItemSize(GC gc) { tab.height = tabHeight; tab.width = width; tab.closeRect.width = tab.closeRect.height = 0; - if (showClose || tab.showClose) { + if (showClose || tab.showClose || (dirtyIndicatorStyle && tab.showDirty)) { Point closeSize = renderer.computeSize(CTabFolderRenderer.PART_CLOSE_BUTTON, SWT.SELECTED, gc, SWT.DEFAULT, SWT.DEFAULT); tab.closeRect.width = closeSize.x; tab.closeRect.height = closeSize.y; @@ -2980,8 +2981,8 @@ boolean setItemSize(GC gc) { tab.height = tabHeight; tab.width = width; tab.closeRect.width = tab.closeRect.height = 0; - if (showClose || tab.showClose) { - if (i == selectedIndex || showUnselectedClose) { + if (showClose || tab.showClose || (dirtyIndicatorStyle && tab.showDirty)) { + if (i == selectedIndex || showUnselectedClose || (dirtyIndicatorStyle && tab.showDirty)) { Point closeSize = renderer.computeSize(CTabFolderRenderer.PART_CLOSE_BUTTON, SWT.NONE, gc, SWT.DEFAULT, SWT.DEFAULT); tab.closeRect.width = closeSize.x; tab.closeRect.height = closeSize.y; @@ -3687,6 +3688,49 @@ public void setUnselectedCloseVisible(boolean visible) { showUnselectedClose = visible; updateFolder(REDRAW); } +/** + * Sets whether the dirty indicator style is enabled. When enabled, + * dirty items (marked via {@link CTabItem#setShowDirty(boolean)}) show a + * bullet dot at the close button location instead of the traditional + * * prefix. The bullet transforms into the close button on hover. + *

+ * The default value is false (traditional * prefix + * behavior). + *

+ * + * @param enabled true to enable the dirty indicator style + * + * @exception SWTException + * + * @see CTabItem#setShowDirty(boolean) + * @since 3.134 + */ +public void setDirtyIndicatorStyle(boolean enabled) { + checkWidget(); + if (dirtyIndicatorStyle == enabled) return; + dirtyIndicatorStyle = enabled; + updateFolder(REDRAW_TABS); +} +/** + * Returns whether the dirty indicator style is enabled. + * + * @return true if the dirty indicator style is enabled + * + * @exception SWTException + * + * @see #setDirtyIndicatorStyle(boolean) + * @since 3.134 + */ +public boolean getDirtyIndicatorStyle() { + checkWidget(); + return dirtyIndicatorStyle; +} /** * Specify whether the image appears on unselected tabs. * @@ -4021,7 +4065,7 @@ String _getToolTip(int x, int y) { CTabItem item = getItem(new Point (x, y)); if (item == null) return null; if (!item.showing) return null; - if ((showClose || item.showClose) && item.closeRect.contains(x, y)) { + if ((showClose || item.showClose || (dirtyIndicatorStyle && item.showDirty)) && item.closeRect.contains(x, y)) { return SWT.getMessage("SWT_Close"); //$NON-NLS-1$ } return item.getToolTipText(); diff --git a/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabFolderRenderer.java b/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabFolderRenderer.java index 9e6164b8dd..4c5a15db95 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabFolderRenderer.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabFolderRenderer.java @@ -363,7 +363,7 @@ protected Point computeSize (int part, int state, GC gc, int wHint, int hHint) { if (shouldApplyLargeTextPadding(parent)) { width += getLargeTextPadding(item) * 2; - } else if (shouldDrawCloseIcon(item)) { + } else if (shouldAllocateCloseRect(item)) { if (width > 0) width += INTERNAL_SPACING; width += computeSize(PART_CLOSE_BUTTON, SWT.NONE, gc, SWT.DEFAULT, SWT.DEFAULT).x; } @@ -383,6 +383,15 @@ private boolean shouldDrawCloseIcon(CTabItem item) { return showClose && isSelectedOrShowCloseForUnselected; } + private boolean shouldDrawDirtyIndicator(CTabItem item) { + CTabFolder folder = item.getParent(); + return folder.dirtyIndicatorStyle && item.showDirty; + } + + private boolean shouldAllocateCloseRect(CTabItem item) { + return shouldDrawCloseIcon(item) || shouldDrawDirtyIndicator(item); + } + /** * Returns padding for the text of a tab item when showing images is disabled for the tab folder. */ @@ -880,8 +889,21 @@ void drawBody(GC gc, Rectangle bounds, int state) { } void drawClose(GC gc, Rectangle closeRect, int closeImageState) { + drawClose(gc, closeRect, closeImageState, false); + } + + void drawClose(GC gc, Rectangle closeRect, int closeImageState, boolean showDirtyIndicator) { if (closeRect.width == 0 || closeRect.height == 0) return; + // When dirty and not hovered/pressed, draw bullet instead of X + if (showDirtyIndicator) { + int maskedState = closeImageState & (SWT.HOT | SWT.SELECTED | SWT.BACKGROUND); + if (maskedState != SWT.HOT && maskedState != SWT.SELECTED) { + drawDirtyIndicator(gc, closeRect); + return; + } + } + // draw X with length of this constant final int lineLength = 8; int x = closeRect.x + Math.max(1, (closeRect.width-lineLength)/2); @@ -912,6 +934,16 @@ void drawClose(GC gc, Rectangle closeRect, int closeImageState) { gc.setForeground(originalForeground); } + private void drawDirtyIndicator(GC gc, Rectangle closeRect) { + int diameter = 8; + int x = closeRect.x + (closeRect.width - diameter) / 2; + int y = closeRect.y + (closeRect.height - diameter) / 2; + Color originalBackground = gc.getBackground(); + gc.setBackground(gc.getForeground()); + gc.fillOval(x, y, diameter, diameter); + gc.setBackground(originalBackground); + } + private void drawCloseLines(GC gc, int x, int y, int lineLength, boolean hot) { if (hot) { gc.setLineWidth(gc.getLineWidth() + 2); @@ -1420,7 +1452,7 @@ void drawSelected(int itemIndex, GC gc, Rectangle bounds, int state ) { // draw Image Rectangle trim = computeTrim(itemIndex, SWT.NONE, 0, 0, 0, 0); int xDraw = x - trim.x; - if (parent.single && shouldDrawCloseIcon(item)) xDraw += item.closeRect.width; + if (parent.single && shouldAllocateCloseRect(item)) xDraw += item.closeRect.width; Image image = item.getImage(); if (image != null && !image.isDisposed() && parent.showSelectedImage) { Rectangle imageBounds = image.getBounds(); @@ -1473,7 +1505,9 @@ void drawSelected(int itemIndex, GC gc, Rectangle bounds, int state ) { gc.setBackground(orginalBackground); } } - if (shouldDrawCloseIcon(item)) drawClose(gc, item.closeRect, item.closeImageState); + if (shouldAllocateCloseRect(item)) { + drawClose(gc, item.closeRect, item.closeImageState, shouldDrawDirtyIndicator(item)); + } } } @@ -1481,7 +1515,7 @@ private int getLeftTextMargin(CTabItem item) { int margin = 0; if (shouldApplyLargeTextPadding(parent)) { margin += getLargeTextPadding(item); - if (shouldDrawCloseIcon(item)) { + if (shouldAllocateCloseRect(item)) { margin -= item.closeRect.width / 2; } } @@ -1646,7 +1680,7 @@ void drawUnselected(int index, GC gc, Rectangle bounds, int state) { Rectangle imageBounds = image.getBounds(); // only draw image if it won't overlap with close button int maxImageWidth = x + width - xDraw - (trim.width + trim.x); - if (shouldDrawCloseIcon(item)) { + if (shouldAllocateCloseRect(item)) { maxImageWidth -= item.closeRect.width + INTERNAL_SPACING; } if (imageBounds.width < maxImageWidth) { @@ -1662,7 +1696,7 @@ void drawUnselected(int index, GC gc, Rectangle bounds, int state) { // draw Text xDraw += getLeftTextMargin(item); int textWidth = x + width - xDraw - (trim.width + trim.x); - if (shouldDrawCloseIcon(item)) { + if (shouldAllocateCloseRect(item)) { textWidth -= item.closeRect.width + INTERNAL_SPACING; } if (textWidth > 0) { @@ -1679,8 +1713,10 @@ void drawUnselected(int index, GC gc, Rectangle bounds, int state) { gc.drawText(item.shortenedText, xDraw, textY, FLAGS); gc.setFont(gcFont); } - // draw close - if (shouldDrawCloseIcon(item)) drawClose(gc, item.closeRect, item.closeImageState); + // draw close or dirty indicator + if (shouldAllocateCloseRect(item)) { + drawClose(gc, item.closeRect, item.closeImageState, shouldDrawDirtyIndicator(item)); + } } } diff --git a/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabItem.java b/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabItem.java index 884c44354d..5b5ac8c7eb 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabItem.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabItem.java @@ -55,6 +55,7 @@ public class CTabItem extends Item { int closeImageState = SWT.BACKGROUND; int state = SWT.NONE; boolean showClose = false; + boolean showDirty = false; boolean showing = false; /** @@ -276,6 +277,26 @@ public boolean getShowClose() { checkWidget(); return showClose; } +/** + * Returns true to indicate that the receiver is dirty + * (has unsaved changes). When the parent folder's dirty indicator style + * is enabled, dirty items show a bullet dot at the close button location + * instead of the default * prefix. + * + * @return true if the item is marked as dirty + * + * @exception SWTException + * + * @see CTabFolder#setDirtyIndicatorStyle(boolean) + * @since 3.134 + */ +public boolean getShowDirty() { + checkWidget(); + return showDirty; +} /** * Returns the receiver's tool tip text, or null if it has * not been set. @@ -490,6 +511,29 @@ public void setShowClose(boolean close) { showClose = close; parent.updateFolder(CTabFolder.REDRAW_TABS); } +/** + * Marks this item as dirty (having unsaved changes). When the parent + * folder's dirty indicator style is enabled via + * {@link CTabFolder#setDirtyIndicatorStyle(boolean)}, dirty items + * show a bullet dot at the close button location. The bullet transforms + * into the close button on hover. + * + * @param dirty true to mark the item as dirty + * + * @exception SWTException + * + * @see CTabFolder#setDirtyIndicatorStyle(boolean) + * @since 3.134 + */ +public void setShowDirty(boolean dirty) { + checkWidget(); + if (showDirty == dirty) return; + showDirty = dirty; + parent.updateFolder(CTabFolder.REDRAW_TABS); +} /** * Sets the text to display on the tab. * A carriage return '\n' allows to display multi line text. diff --git a/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet393.java b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet393.java new file mode 100644 index 0000000000..0f6115be3a --- /dev/null +++ b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet393.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * 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 + *******************************************************************************/ +package org.eclipse.swt.snippets; + +/* + * CTabFolder example: dirty indicator using bullet dot on close button, + * with runtime dark/light theme switching to show color adaptation. + * + * For a list of all SWT example snippets see + * http://www.eclipse.org/swt/snippets/ + */ +import org.eclipse.swt.*; +import org.eclipse.swt.custom.*; +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.layout.*; +import org.eclipse.swt.widgets.*; + +public class Snippet393 { + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setLayout(new GridLayout()); + shell.setText("CTabFolder Dirty Indicator"); + + CTabFolder folder = new CTabFolder(shell, SWT.CLOSE | SWT.BORDER); + folder.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + folder.setDirtyIndicatorStyle(true); + + for (int i = 0; i < 4; i++) { + CTabItem item = new CTabItem(folder, SWT.NONE); + item.setText("Tab " + i); + Text text = new Text(folder, SWT.MULTI | SWT.WRAP); + text.setText("Content for tab " + i); + item.setControl(text); + } + + // Mark tabs 0 and 2 as dirty + folder.getItem(0).setShowDirty(true); + folder.getItem(2).setShowDirty(true); + folder.setSelection(0); + + Button toggleButton = new Button(shell, SWT.PUSH); + toggleButton.setText("Toggle dirty on selected tab"); + toggleButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + toggleButton.addListener(SWT.Selection, e -> { + CTabItem selected = folder.getSelection(); + if (selected != null) { + selected.setShowDirty(!selected.getShowDirty()); + } + }); + + boolean[] isDark = {false}; + Button toggleThemeButton = new Button(shell, SWT.PUSH); + toggleThemeButton.setText("Switch to Dark Theme"); + toggleThemeButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + toggleThemeButton.addListener(SWT.Selection, e -> { + isDark[0] = !isDark[0]; + if (isDark[0]) { + Color tabBg = new Color(display, 43, 43, 43); + Color selBg = new Color(display, 60, 63, 65); + Color contentBg = new Color(display, 30, 30, 30); + Color fg = new Color(display, 187, 187, 187); + folder.setBackground(tabBg); + folder.setForeground(fg); + folder.setSelectionBackground(selBg); + folder.setSelectionForeground(fg); + for (int i = 0; i < folder.getItemCount(); i++) { + Control ctrl = folder.getItem(i).getControl(); + if (ctrl != null) { + ctrl.setBackground(contentBg); + ctrl.setForeground(fg); + } + } + toggleThemeButton.setText("Switch to Light Theme"); + } else { + folder.setBackground(null); + folder.setForeground(null); + folder.setSelectionBackground((Color) null); + folder.setSelectionForeground(null); + for (int i = 0; i < folder.getItemCount(); i++) { + Control ctrl = folder.getItem(i).getControl(); + if (ctrl != null) { + ctrl.setBackground(null); + ctrl.setForeground(null); + } + } + toggleThemeButton.setText("Switch to Dark Theme"); + } + }); + + shell.setSize(400, 300); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + display.dispose(); + } +} diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_CTabItem.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_CTabItem.java index 25fe59923a..5ecb72fe6b 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_CTabItem.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_CTabItem.java @@ -14,6 +14,8 @@ package org.eclipse.swt.tests.junit; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CTabFolder; @@ -76,4 +78,22 @@ public void test_setSelectionForegroundLorg_eclipse_swt_graphics_Color() { cTabItem.setSelectionForeground(null); assertEquals(red, cTabItem.getSelectionForeground()); } -} \ No newline at end of file + +@Test +public void test_setShowDirty() { + assertFalse(cTabItem.getShowDirty()); + cTabItem.setShowDirty(true); + assertTrue(cTabItem.getShowDirty()); + cTabItem.setShowDirty(false); + assertFalse(cTabItem.getShowDirty()); +} + +@Test +public void test_dirtyIndicatorCloseStyle() { + assertFalse(cTabFolder.getDirtyIndicatorStyle()); + cTabFolder.setDirtyIndicatorStyle(true); + assertTrue(cTabFolder.getDirtyIndicatorStyle()); + cTabFolder.setDirtyIndicatorStyle(false); + assertFalse(cTabFolder.getDirtyIndicatorStyle()); +} +}