diff --git a/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/internal/workbench/renderers/swt/BasicPartList.java b/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/internal/workbench/renderers/swt/BasicPartList.java index 83df2a75389..63f57378ec1 100644 --- a/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/internal/workbench/renderers/swt/BasicPartList.java +++ b/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/internal/workbench/renderers/swt/BasicPartList.java @@ -72,11 +72,12 @@ public Font getFont(Object element) { @Override public String getText(Object element) { - if (element instanceof MDirtyable - && ((MDirtyable) element).isDirty()) { - return "*" + ((MUILabel) element).getLocalizedLabel(); //$NON-NLS-1$ + String label = ((MUILabel) element).getLocalizedLabel(); + if (element instanceof MDirtyable && ((MDirtyable) element).isDirty() + && !DirtyIndicatorPainter.isEnabled()) { + return "*" + label; //$NON-NLS-1$ } - return ((MUILabel) element).getLocalizedLabel(); + return label; } @Override @@ -146,6 +147,9 @@ protected TableViewer createTableViewer(Composite parent, int style) { tableViewer.setContentProvider(ArrayContentProvider.getInstance()); tableViewer.setLabelProvider(new BasicStackListLabelProvider()); + DirtyIndicatorPainter.install(table, + data -> data instanceof MDirtyable d && d.isDirty()); + ColumnViewerToolTipSupport.enableFor(tableViewer); return tableViewer; } diff --git a/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/internal/workbench/renderers/swt/DirtyIndicatorPainter.java b/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/internal/workbench/renderers/swt/DirtyIndicatorPainter.java new file mode 100644 index 00000000000..0b881e2ccd9 --- /dev/null +++ b/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/internal/workbench/renderers/swt/DirtyIndicatorPainter.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * + * 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.e4.ui.internal.workbench.renderers.swt; + +import java.util.function.Predicate; +import org.eclipse.core.runtime.Platform; +import org.eclipse.e4.ui.workbench.renderers.swt.CTabRendering; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +/** + * Paints a filled circle in a right-aligned column for dirty rows in editor + * list popups (Ctrl+E quick switch, chevron drop-down, ...) so they match the + * close-button overlay drawn by {@code CTabFolderRenderer.drawDirtyIndicator} + * for tabs. Right-aligning makes it easy to scan the list for dirty editors. + */ +public final class DirtyIndicatorPainter { + + // Diameter and color match CTabFolderRenderer.drawDirtyIndicator so the + // indicator looks identical to the close-button overlay used on tabs. + private static final int DIAMETER = 8; + + // Padding on either side of the dot so it does not crowd the text or the + // cell's right edge. + private static final int PADDING = DIAMETER; + + private DirtyIndicatorPainter() { + } + + /** + * @return whether the {@link CTabRendering#SHOW_DIRTY_INDICATOR_ON_TABS new + * dirty indicator style} is enabled + */ + public static boolean isEnabled() { + return Platform.getPreferencesService().getBoolean( + CTabRendering.PREF_QUALIFIER_ECLIPSE_E4_UI_WORKBENCH_RENDERERS_SWT, + CTabRendering.SHOW_DIRTY_INDICATOR_ON_TABS, + CTabRendering.SHOW_DIRTY_INDICATOR_ON_TABS_DEFAULT, null); + } + + /** + * Adds {@link SWT#MeasureItem} and {@link SWT#PaintItem} listeners to + * {@code table}. The measure listener reserves space at the right of every + * row so the dots line up in a column and do not crowd the text. The paint + * listener draws a filled circle right-aligned in that reserved column for + * rows whose data passes {@code isDirty}. Both listeners are no-ops while + * {@link #isEnabled()} returns {@code false}, so callers can install them + * unconditionally. + */ + public static void install(Table table, Predicate isDirty) { + Listener listener = event -> { + if (!isEnabled()) { + return; + } + if (!(event.item instanceof TableItem item)) { + return; + } + if (event.type == SWT.MeasureItem) { + // Reserve space for the dot column on every row so the column + // width packs wide enough and all dots align. + event.width += PADDING + DIAMETER + PADDING; + return; + } + if (!isDirty.test(item.getData())) { + return; + } + GC gc = event.gc; + Rectangle cellBounds = item.getBounds(event.index); + int x = cellBounds.x + cellBounds.width - DIAMETER - PADDING; + int y = cellBounds.y + (cellBounds.height - DIAMETER) / 2; + Color originalBackground = gc.getBackground(); + int originalAntialias = gc.getAntialias(); + gc.setBackground(gc.getForeground()); + gc.setAntialias(SWT.ON); + gc.fillOval(x, y, DIAMETER, DIAMETER); + gc.setBackground(originalBackground); + gc.setAntialias(originalAntialias); + }; + table.addListener(SWT.MeasureItem, listener); + table.addListener(SWT.PaintItem, listener); + } +} diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbookEditorsHandler.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbookEditorsHandler.java index af2b0fe19de..2d390dda309 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbookEditorsHandler.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbookEditorsHandler.java @@ -32,6 +32,7 @@ import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.e4.ui.internal.workbench.renderers.swt.DirtyIndicatorPainter; import org.eclipse.e4.ui.model.application.ui.basic.MPart; import org.eclipse.e4.ui.workbench.renderers.swt.CTabRendering; import org.eclipse.e4.ui.workbench.renderers.swt.StackRenderer; @@ -286,19 +287,30 @@ private Path getPathSegment(Integer segmentIndex, java.nio.file.Path path) { } /** - * Prepends a {@code *} to the labelText if editorReference is dirty. + * Prepends a {@code *} to the labelText if editorReference is dirty. When the + * {@link CTabRendering#SHOW_DIRTY_INDICATOR_ON_TABS new dirty indicator style} + * is enabled the prefix is omitted, because {@link DirtyIndicatorPainter} + * paints a filled circle next to the title instead. * * @param editorReference reference to check for dirty state * @param labelText the label text for the editorReference * @return text with dirty indication when appropriate */ private String prependDirtyIndicationIfDirty(EditorReference editorReference, String labelText) { - if (editorReference.isDirty()) { + if (editorReference.isDirty() && !DirtyIndicatorPainter.isEnabled()) { return DIRTY_PREFIX + labelText; } return labelText; } + @Override + protected String getWorkbenchPartReferenceText(WorkbenchPartReference ref) { + if (DirtyIndicatorPainter.isEnabled()) { + return ref.getTitle(); + } + return super.getWorkbenchPartReferenceText(ref); + } + /** * Returns a count of the number of segments which match in this path and the * given path (device ids are ignored), comparing in decreasing segment number @@ -401,6 +413,8 @@ public void dispose() { }); ColumnViewerToolTipSupport.enableFor(tableViewerColumn.getViewer()); + DirtyIndicatorPainter.install(((TableViewer) tableViewerColumn.getViewer()).getTable(), + data -> data instanceof EditorReference ref && ref.isDirty()); } /** True if the given model represents the active editor */