From fe2292af124c2b0b3672227612d5418ecdb43045 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Sat, 5 Sep 2026 03:55:17 +0200 Subject: [PATCH] Add a Themes submenu to Window > Appearance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switching the theme so far meant opening the appearance preferences. The submenu lists the installed themes as radio items, applies the pick right away and remembers it for the next start. A light/dark switch offers a restart, and a last entry records the active theme as the default for new workspaces. The items are built in the workbench, where the theme engine is visible, and instantiated from the IDE menu through ExtensionFactory like the Show In menu. Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- bundles/org.eclipse.ui.ide/plugin.properties | 2 + bundles/org.eclipse.ui.ide/plugin.xml | 13 ++ .../org/eclipse/ui/ExtensionFactory.java | 10 ++ .../internal/menus/ThemeContributionItem.java | 121 ++++++++++++++++++ 4 files changed, 146 insertions(+) create mode 100644 bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/menus/ThemeContributionItem.java 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()); + } + } +}