diff --git a/bundles/org.eclipse.ui.ide/plugin.properties b/bundles/org.eclipse.ui.ide/plugin.properties
index ad8338928c5..c48a4dc3fc2 100644
--- a/bundles/org.eclipse.ui.ide/plugin.properties
+++ b/bundles/org.eclipse.ui.ide/plugin.properties
@@ -343,6 +343,8 @@ multiFilterProvider.description = Match many attributes of files and folders
menu.window.appearance.label=Appearance
menu.window.appearance.mnemonic=a
menu.window.appearance.tooltip=Change appearance of Windows and Toolbars
+menu.window.appearance.themes.label=Themes
+menu.window.appearance.themes.mnemonic=T
menu.copyDetails.label=Cop&y Details
command.edit.copy.mnemonic = C
diff --git a/bundles/org.eclipse.ui.ide/plugin.xml b/bundles/org.eclipse.ui.ide/plugin.xml
index 8ee52ff8427..02c99caee6f 100644
--- a/bundles/org.eclipse.ui.ide/plugin.xml
+++ b/bundles/org.eclipse.ui.ide/plugin.xml
@@ -1794,6 +1794,19 @@
commandId="org.eclipse.ui.window.minimizePart"
style="push">
+
+
+
diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/ExtensionFactory.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/ExtensionFactory.java
index 577d744bc50..250740bdf58 100644
--- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/ExtensionFactory.java
+++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/ExtensionFactory.java
@@ -30,6 +30,7 @@
import org.eclipse.ui.internal.dialogs.WorkbenchPreferencePage;
import org.eclipse.ui.internal.keys.KeysPreferencePage;
import org.eclipse.ui.internal.keys.NewKeysPreferencePage;
+import org.eclipse.ui.internal.menus.ThemeContributionItem;
import org.eclipse.ui.internal.progress.ProgressView;
import org.eclipse.ui.internal.themes.ColorsAndFontsPreferencePage;
import org.eclipse.ui.internal.wizards.preferences.PreferencesExportWizard;
@@ -120,6 +121,12 @@ public class ExtensionFactory implements IExecutableExtensionFactory, IExecutabl
*/
public static final String SHOW_IN_CONTRIBUTION = "showInContribution"; //$NON-NLS-1$
+ /**
+ * Factory ID for the contribution listing the installed themes. Private on
+ * purpose, a public constant here would be new API.
+ */
+ private static final String THEME_CONTRIBUTION = "themeContribution"; //$NON-NLS-1$
+
private IConfigurationElement config;
private String id;
@@ -188,6 +195,9 @@ public Object create() throws CoreException {
if (SHOW_IN_CONTRIBUTION.equals(id)) {
return new ShowInMenu();
}
+ if (THEME_CONTRIBUTION.equals(id)) {
+ return new ThemeContributionItem();
+ }
throw new CoreException(new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, 0,
"Unknown id in data argument for " + getClass(), null)); //$NON-NLS-1$
diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/menus/ThemeContributionItem.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/menus/ThemeContributionItem.java
new file mode 100644
index 00000000000..118d3eaa807
--- /dev/null
+++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/menus/ThemeContributionItem.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * 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
+ *
+ * Contributors:
+ * Lars Vogel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.ui.internal.menus;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+import org.eclipse.e4.ui.css.swt.theme.ITheme;
+import org.eclipse.e4.ui.css.swt.theme.IThemeEngine;
+import org.eclipse.jface.action.ContributionItem;
+import org.eclipse.jface.action.IContributionItem;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.actions.CompoundContributionItem;
+import org.eclipse.ui.internal.WorkbenchMessages;
+import org.eclipse.ui.internal.themes.DefaultThemePreference;
+
+/**
+ * Lists the installed themes and applies the one the user picks, remembering it
+ * for the next start. A last entry records the active theme as the default for
+ * new workspaces.
+ */
+public class ThemeContributionItem extends CompoundContributionItem {
+
+ public ThemeContributionItem() {
+ }
+
+ public ThemeContributionItem(String id) {
+ super(id);
+ }
+
+ @Override
+ protected IContributionItem[] getContributionItems() {
+ IThemeEngine engine = PlatformUI.getWorkbench().getService(IThemeEngine.class);
+ // High contrast mode pins the engine to the high contrast theme, so offer no choice.
+ if (engine == null || PlatformUI.getWorkbench().getDisplay().getHighContrast()) {
+ return new IContributionItem[0];
+ }
+ List themes = new ArrayList<>(engine.getThemes());
+ themes.sort(Comparator.comparing(ITheme::getLabel, String.CASE_INSENSITIVE_ORDER));
+
+ List items = new ArrayList<>();
+ themes.forEach(theme -> items.add(createItem(engine, theme)));
+ if (engine.getActiveTheme() != null) {
+ items.add(new Separator());
+ items.add(createSetAsDefaultItem(engine));
+ }
+ return items.toArray(IContributionItem[]::new);
+ }
+
+ /** Records the active theme as the default for new workspaces. */
+ private static IContributionItem createSetAsDefaultItem(IThemeEngine engine) {
+ return new ContributionItem() {
+ @Override
+ public void fill(Menu menu, int index) {
+ MenuItem item = new MenuItem(menu, SWT.PUSH, index);
+ item.setText(WorkbenchMessages.ThemeDefault_setDefault);
+ item.addListener(SWT.Selection, event -> {
+ ITheme activeTheme = engine.getActiveTheme();
+ if (activeTheme != null) {
+ DefaultThemePreference.set(activeTheme);
+ }
+ });
+ }
+ };
+ }
+
+ private static IContributionItem createItem(IThemeEngine engine, ITheme theme) {
+ return new ContributionItem() {
+ @Override
+ public void fill(Menu menu, int index) {
+ MenuItem item = new MenuItem(menu, SWT.RADIO, index);
+ item.setText(theme.getLabel());
+ ITheme activeTheme = engine.getActiveTheme();
+ item.setSelection(activeTheme != null && activeTheme.getId().equals(theme.getId()));
+ item.addListener(SWT.Selection, event -> {
+ if (!((MenuItem) event.widget).getSelection()) {
+ return;
+ }
+ ITheme previousTheme = engine.getActiveTheme();
+ // Clicking the checked entry fires a selection too, reapplying it would
+ // restyle the whole workbench for nothing.
+ if (previousTheme != null && previousTheme.getId().equals(theme.getId())) {
+ return;
+ }
+ engine.setTheme(theme, true);
+ // Only a light/dark switch leaves parts styled for the previous appearance.
+ if (previousTheme != null && previousTheme.isDark() != theme.isDark()) {
+ offerRestart();
+ }
+ });
+ }
+ };
+ }
+
+ private static void offerRestart() {
+ MessageDialog dialog = new MessageDialog(null, WorkbenchMessages.ThemeChangeWarningTitle, null,
+ WorkbenchMessages.ThemeChangeWarningText, MessageDialog.NONE, 1,
+ WorkbenchMessages.Workbench_RestartButton, WorkbenchMessages.Workbench_DontRestartButton);
+ if (dialog.open() == 0) {
+ Display.getDefault().asyncExec(() -> PlatformUI.getWorkbench().restart());
+ }
+ }
+}