From d2252ee28d42bb51b57a6315b178e8edd98a7ec3 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Tue, 6 Oct 2026 10:57:36 +0200 Subject: [PATCH] Style CTabFolder pages that are hidden when they are skinned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Search dialog opened unstyled until a tab was switched. A CTabFolder exposes only the page of its selected tab to the CSS engine, and applyStyles skips an element that is not visible. A page skinned while its tab is not selected, or before it is attached to its tab, as SearchDialog and its search pages do, is skipped, and neither the programmatic setSelection nor the attach sends a skin event that would restyle it. Watch such a page for the show or resize that makes it the selected tab's page and style it then. Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../swt/engine/CSSSWTApplyStylesListener.java | 55 +++++++++++++-- .../e4/ui/tests/css/swt/CTabFolderTest.java | 68 +++++++++++++++++++ 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTApplyStylesListener.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTApplyStylesListener.java index 29a5a7ed714..26ac37db0e4 100644 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTApplyStylesListener.java +++ b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTApplyStylesListener.java @@ -15,22 +15,69 @@ import org.eclipse.e4.ui.css.core.engine.CSSEngine; import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CTabFolder; +import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; /** - * Add SWT filter to the {@link Display} to apply styles when SWT widget is - * resized or showed. + * Applies styles to the widgets of a {@link Display} when they are skinned, and + * to a {@link CTabFolder} page the engine skipped once it becomes the selected tab's page. */ public class CSSSWTApplyStylesListener { + // marks a control this listener already watches, since every re-skin repeats the skin event + private final String pendingPageKey = getClass().getName() + '@' + Integer.toHexString(hashCode()); + CSSEngine engine; public CSSSWTApplyStylesListener(Display display, final CSSEngine engine) { this.engine = engine; display.addListener(SWT.Skin, event -> { - if (engine != null) { - engine.applyStyles(event.widget, false); + if (engine == null) { + return; + } + engine.applyStyles(event.widget, false); + if (event.widget instanceof Control control && control.getParent() instanceof CTabFolder folder + && control != folder.getTopRight() && !isPageOfSelectedTab(folder, control)) { + // the folder hides this page from the engine, so it was skipped + styleWhenItBecomesThePage(folder, control); } }); } + /** + * Styles the control once it is the selected tab's page, which attaching it + * resizes it to or selecting its tab shows it. + */ + private void styleWhenItBecomesThePage(CTabFolder folder, Control control) { + if (control.getData(pendingPageKey) != null) { + return; + } + Listener listener = new Listener() { + @Override + public void handleEvent(Event event) { + // the control may have been reparented, and a new skin event watches it again + boolean inFolder = control.getParent() == folder && !folder.isDisposed(); + if (inFolder && !isPageOfSelectedTab(folder, control)) { + return; + } + control.removeListener(SWT.Show, this); + control.removeListener(SWT.Resize, this); + control.setData(pendingPageKey, null); + if (inFolder) { + engine.applyStyles(control, true); + } + } + }; + control.setData(pendingPageKey, listener); + control.addListener(SWT.Show, listener); + control.addListener(SWT.Resize, listener); + } + + private static boolean isPageOfSelectedTab(CTabFolder folder, Control control) { + int selected = folder.getSelectionIndex(); + return selected >= 0 && folder.getItem(selected).getControl() == control; + } + } diff --git a/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/CTabFolderTest.java b/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/CTabFolderTest.java index 9d25435803e..d7c2258dc35 100644 --- a/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/CTabFolderTest.java +++ b/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/CTabFolderTest.java @@ -22,8 +22,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotSame; +import java.io.IOException; +import java.io.StringReader; + import org.eclipse.e4.ui.css.core.engine.CSSEngine; import org.eclipse.e4.ui.css.swt.dom.WidgetElement; +import org.eclipse.e4.ui.css.swt.engine.CSSSWTEngineImpl; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CTabFolder; import org.eclipse.swt.custom.CTabItem; @@ -356,4 +360,68 @@ void testMinimumCharacters() { assertEquals(1, folderToTest.getMinimumCharacters()); assertEquals("1", css.getEngine().retrieveCSSProperty(folderToTest, "swt-tab-text-minimum-characters", null)); } + + @Test + void testPageSelectedProgrammaticallyAfterSkinningIsStyled() throws IOException { + Display display = css.getDisplay(); + CSSEngine engine = new CSSSWTEngineImpl(display, true); + // a class selector keeps this engine, which stays attached to the shared + // display as a skin listener, from styling the widgets of other tests + engine.parseStyleSheet(new StringReader(".tabPage { background-color: #FF0000 }")); + + Shell shell = new Shell(display, SWT.SHELL_TRIM); + shell.setLayout(new FillLayout()); + CTabFolder folderToTest = new CTabFolder(shell, SWT.NONE); + CTabItem tab1 = new CTabItem(folderToTest, SWT.NONE); + tab1.setText("A TAB ITEM"); + // nothing is selected yet, so the page is hidden and the CTabFolder does + // not expose it to the engine + Composite page = new Composite(folderToTest, SWT.NONE); + WidgetElement.setCSSClass(page, "tabPage"); + tab1.setControl(page); + spinEventLoop(display); // the skin pass skips the hidden page + + // a programmatic selection sends no selection event, so only the page + // becoming visible can trigger its styling + folderToTest.setSelection(0); + spinEventLoop(display); + + assertEquals(RED, page.getBackground().getRGB()); + } + + @Test + void testPageAttachedToItsItemAfterSkinningIsStyled() throws IOException { + Display display = css.getDisplay(); + CSSEngine engine = new CSSSWTEngineImpl(display, true); + engine.parseStyleSheet(new StringReader(".tabPage { background-color: #FF0000 }")); + + Shell shell = new Shell(display, SWT.SHELL_TRIM); + shell.setLayout(new FillLayout()); + CTabFolder folderToTest = new CTabFolder(shell, SWT.NONE); + CTabItem tab1 = new CTabItem(folderToTest, SWT.NONE); + tab1.setText("A TAB ITEM"); + folderToTest.setSelection(0); + shell.setSize(400, 300); + shell.layout(true, true); + + Composite page = new Composite(folderToTest, SWT.NONE); + WidgetElement.setCSSClass(page, "tabPage"); + // building the page runs the event loop, as loading a search page does in + // the Search dialog, so the page is skinned while its item has no control + // yet and the CTabFolder does not expose it + spinEventLoop(display); + + // the tab is already selected and the page has been visible since it was + // created, so attaching it sends no show event either + tab1.setControl(page); + spinEventLoop(display); + + assertEquals(RED, page.getBackground().getRGB()); + } + + private static void spinEventLoop(Display display) { + while (display.readAndDispatch()) { + // deliver the pending skin and show events + } + } }