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
+ * - ERROR_WIDGET_DISPOSED - if the receiver has been disposed
+ * - ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver
+ *
+ *
+ * @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
+ * - ERROR_WIDGET_DISPOSED - if the receiver has been disposed
+ * - ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver
+ *
+ *
+ * @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
+ * - ERROR_WIDGET_DISPOSED - if the receiver has been disposed
+ * - ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver
+ *
+ *
+ * @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
+ * - ERROR_WIDGET_DISPOSED - if the receiver has been disposed
+ * - ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver
+ *
+ *
+ * @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());
+}
+}