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 + } + } }