Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
* <code>*</code> prefix. The bullet transforms into the close button on hover.
* <p>
* The default value is <code>false</code> (traditional <code>*</code> prefix
* behavior).
* </p>
*
* @param enabled <code>true</code> to enable the dirty indicator style
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*
* @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 <code>true</code> if the dirty indicator style is enabled
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*
* @see #setDirtyIndicatorStyle(boolean)
* @since 3.134
*/
public boolean getDirtyIndicatorStyle() {
checkWidget();
return dirtyIndicatorStyle;
}
/**
* Specify whether the image appears on unselected tabs.
*
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -1473,15 +1505,17 @@ 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));
}
}
}

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;
}
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -276,6 +277,26 @@ public boolean getShowClose() {
checkWidget();
return showClose;
}
/**
* Returns <code>true</code> 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 <code>*</code> prefix.
*
* @return <code>true</code> if the item is marked as dirty
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*
* @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.
Expand Down Expand Up @@ -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 <code>true</code> to mark the item as dirty
*
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*
* @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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading
Loading