From 8c91d0163ba4eb6a8d62a54132222698e556b93c Mon Sep 17 00:00:00 2001 From: andi-huber Date: Tue, 26 May 2026 19:07:38 +0200 Subject: [PATCH 1/2] CAUSEWAY-4001: fixes wicket viewer theme picking for cases when 'default' theme is included also fixes active marker on selected theme however, introduces some static resource not available exceptions logged --- .../ui/CausewayModuleViewerWicketUi.java | 62 +++++++- .../CausewayWicketThemeSupport.java | 102 ++++++++++++- .../CausewayWicketThemeSupportDefault.java | 131 ----------------- .../widgets/themepicker/ThemeChooser.java | 138 ++++++++---------- .../themepicker/ThemeProviderComposite.java | 113 -------------- .../wicketapp/config/BootstrapInitWkt.java | 9 +- 6 files changed, 219 insertions(+), 336 deletions(-) delete mode 100644 viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/CausewayWicketThemeSupportDefault.java delete mode 100644 viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/ThemeProviderComposite.java diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/CausewayModuleViewerWicketUi.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/CausewayModuleViewerWicketUi.java index 0d13297f211..90a70cbe61d 100644 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/CausewayModuleViewerWicketUi.java +++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/CausewayModuleViewerWicketUi.java @@ -18,27 +18,83 @@ */ package org.apache.causeway.viewer.wicket.ui; +import java.util.List; + +import org.apache.wicket.markup.head.CssHeaderItem; +import org.apache.wicket.markup.head.HeaderItem; +import org.apache.wicket.markup.head.IHeaderResponse; + +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.apache.causeway.viewer.commons.services.CausewayModuleViewerCommonsServices; import org.apache.causeway.viewer.wicket.model.CausewayModuleViewerWicketModel; import org.apache.causeway.viewer.wicket.ui.app.logout.LogoutHandlerWkt; -import org.apache.causeway.viewer.wicket.ui.components.widgets.themepicker.CausewayWicketThemeSupportDefault; +import org.apache.causeway.viewer.wicket.ui.components.widgets.themepicker.CausewayWicketThemeSupport; + +import de.agilecoders.wicket.core.Bootstrap; +import de.agilecoders.wicket.core.settings.ITheme; +import de.agilecoders.wicket.core.settings.NoopThemeProvider; +import de.agilecoders.wicket.core.settings.ThemeProvider; +import de.agilecoders.wicket.themes.markup.html.bootswatch.BootswatchTheme; +import de.agilecoders.wicket.themes.markup.html.bootswatch.BootswatchThemeProvider; /** * @since 1.x {@index} */ -@Configuration +@Configuration(proxyBeanMethods = false) @Import({ // Modules CausewayModuleViewerCommonsServices.class, CausewayModuleViewerWicketModel.class, // @Service's - CausewayWicketThemeSupportDefault.class, + CausewayWicketThemeSupport.class, LogoutHandlerWkt.class, }) public class CausewayModuleViewerWicketUi { + @Bean + ThemeProvider bootstrapDefaultThemeProvider() { + return new BootstrapDefaultThemeProvider(); + } + + @Bean + ThemeProvider bootswatchThemeProvider() { + return new BootswatchThemeProvider(BootswatchTheme.Flatly); + } + + /** + * Default Bootstrap Theme + * + * @see NoopThemeProvider + */ + private static class BootstrapDefaultThemeProvider implements ThemeProvider { + private final ITheme theme = new BootstrapDefaultTheme(); + @Override public ITheme byName(final String name) { + return theme; + } + @Override public List available() { + return List.of(theme); + } + @Override public ITheme defaultTheme() { + return theme; + } + private static final class BootstrapDefaultTheme implements ITheme { + @Override public String name() { + return "Default"; + } + @Override public List getDependencies() { + return List.of(); + } + @Override public void renderHead(final IHeaderResponse response) { + response.render(CssHeaderItem.forReference(Bootstrap.getSettings().getCssResourceReference())); + } + @Override public Iterable getCdnUrls() { + return List.of(); + } + } + } + } diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/CausewayWicketThemeSupport.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/CausewayWicketThemeSupport.java index 2ac7d53002c..f83989f1f56 100644 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/CausewayWicketThemeSupport.java +++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/CausewayWicketThemeSupport.java @@ -18,18 +18,112 @@ */ package org.apache.causeway.viewer.wicket.ui.components.widgets.themepicker; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import jakarta.inject.Inject; +import jakarta.inject.Named; + +import org.apache.wicket.util.string.Strings; + +import org.springframework.stereotype.Service; + +import org.apache.causeway.applib.services.registry.ServiceRegistry; +import org.apache.causeway.commons.internal.base._NullSafe; +import org.apache.causeway.core.config.CausewayConfiguration; + +import lombok.extern.slf4j.Slf4j; + +import de.agilecoders.wicket.core.settings.ITheme; +import de.agilecoders.wicket.core.settings.NoopThemeProvider; import de.agilecoders.wicket.core.settings.ThemeProvider; /** * @since 2.0 */ -public interface CausewayWicketThemeSupport { +@Service +@Named("causeway.viewer.wicket.CausewayWicketThemeSupport") +@Slf4j +public record CausewayWicketThemeSupport( + ITheme defaultTheme, + Map themesByName) { + + @Inject + public CausewayWicketThemeSupport(final CausewayConfiguration configuration, final ServiceRegistry serviceRegistry) { + this(configuration, collectThemes(configuration, serviceRegistry)); + } + + private CausewayWicketThemeSupport(final CausewayConfiguration configuration, final Map themesByName) { + this( + Optional.ofNullable(themesByName.get(configuration.viewer().wicket().themes().initial())) + .orElseGet(()->new NoopThemeProvider().defaultTheme()), + themesByName); + } + + public ITheme byName(final String name) { + if (!Strings.isEmpty(name)) { + var theme = themesByName.get(name.toLowerCase()); + if(theme!=null) + return theme; + } + + log.warn("'{}' theme not found amoung enabled {}, " + + "falling back to '{}'", + name, + available().stream().map(ITheme::name).collect(Collectors.joining(", ")), + defaultTheme.name()); + + return defaultTheme; + } + + public List available() { + return themesByName.values() + .stream() + .toList(); + } + + public List availableNames() { + return available() + .stream() + .map(ITheme::name) + .toList(); + } + + public ThemeProvider getThemeProvider() { + return new ThemeProvider() { + @Override public ITheme defaultTheme() { + return defaultTheme; + } + @Override public ITheme byName(final String name) { + return CausewayWicketThemeSupport.this.byName(name); + } + @Override public List available() { + return CausewayWicketThemeSupport.this.available(); + } + }; + } + + // -- HELPER - // -- INTERFACE + private static Map collectThemes( + final CausewayConfiguration configuration, + final ServiceRegistry serviceRegistry) { + var enabledThemeNamesLowercase = _NullSafe.stream(configuration.viewer().wicket().themes().enabled()) + .map(String::toLowerCase) + .collect(Collectors.toCollection(HashSet::new)); - ThemeProvider getThemeProvider(); - List getEnabledThemeNames(); + var themesByName = new LinkedHashMap(); + serviceRegistry.select(ThemeProvider.class).stream() + .map(ThemeProvider::available) + .flatMap(List::stream) + .filter(theme->enabledThemeNamesLowercase.contains(theme.name().toLowerCase())) + .forEach(theme-> + themesByName.put(theme.name().toLowerCase(), theme)); + return themesByName; + } } diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/CausewayWicketThemeSupportDefault.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/CausewayWicketThemeSupportDefault.java deleted file mode 100644 index a278700cbad..00000000000 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/CausewayWicketThemeSupportDefault.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.causeway.viewer.wicket.ui.components.widgets.themepicker; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import jakarta.annotation.Priority; -import jakarta.inject.Inject; -import jakarta.inject.Named; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Service; - -import org.apache.causeway.applib.annotation.PriorityPrecedence; -import org.apache.causeway.applib.services.registry.ServiceRegistry; -import org.apache.causeway.commons.collections.Can; -import org.apache.causeway.commons.internal.base._Lazy; -import org.apache.causeway.core.config.CausewayConfiguration; - -import lombok.extern.slf4j.Slf4j; - -import de.agilecoders.wicket.core.settings.NoopThemeProvider; -import de.agilecoders.wicket.core.settings.ThemeProvider; -import de.agilecoders.wicket.themes.markup.html.bootswatch.BootswatchTheme; -import de.agilecoders.wicket.themes.markup.html.bootswatch.BootswatchThemeProvider; - -/** - * @since 2.0 - */ -@Service -@Named("causeway.viewer.wicket.CausewayWicketThemeSupportDefault") -@Priority(PriorityPrecedence.MIDPOINT) -@Qualifier("Default") -@Slf4j -public class CausewayWicketThemeSupportDefault implements CausewayWicketThemeSupport { - - private final _Lazy themeProvider = _Lazy.threadSafe(this::createThemeProvider); - - @Inject private CausewayConfiguration configuration; - @Inject private ServiceRegistry serviceRegistry; - - @Override - public ThemeProvider getThemeProvider() { - return themeProvider.get(); - } - - @Override - public List getEnabledThemeNames() { - - var composite = themeProvider.get(); - - List allThemes = composite.availableNames(); - - allThemes = filterThemes(allThemes); - - return allThemes; - } - - // -- HELPER - - private ThemeProviderComposite createThemeProvider() { - - var providerBeans = serviceRegistry.select(ThemeProvider.class); - if (providerBeans.isEmpty()) { - return ThemeProviderComposite.of(Can.ofSingleton(createFallbackThemeProvider())); - } - - return ThemeProviderComposite.of(providerBeans); - } - - private ThemeProvider createFallbackThemeProvider() { - var themeName = configuration.viewer().wicket().themes().initial(); - if ("default".equalsIgnoreCase(themeName)) { - // in effect uses the bootstrap 'default' theme - return new NoopThemeProvider(); - } - BootswatchTheme bootswatchTheme; - try { - bootswatchTheme = BootswatchTheme.valueOf(themeName); - } catch (Exception ex) { - bootswatchTheme = BootswatchTheme.Flatly; - log.warn("Did not recognise configured bootswatch theme '{}', defaulting to '{}'", - themeName, - bootswatchTheme); - - } - - return new BootswatchThemeProvider(bootswatchTheme); - } - - /** - * Filters which themes to show in the drop up by using the provided values - * in {@link CausewayConfiguration.Viewer.Wicket.Themes#getEnabled()} - * - * @param availableThemes All available themes - * @return A list of all enabled themes - */ - private List filterThemes(final List availableThemes) { - - var configuredThemes = configuration.viewer().wicket().themes().enabled(); - if (configuredThemes == null || configuredThemes.isEmpty()) { - return Collections.emptyList(); - } - - var enabledThemes = new ArrayList(); - availableThemes.stream() - .filter(availableTheme -> configuredThemes.stream().anyMatch(configuredTheme -> configuredTheme.equalsIgnoreCase(availableTheme))) - .forEach(enabledThemes::add); - - return enabledThemes; - } - -} diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/ThemeChooser.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/ThemeChooser.java index b1e4aa833d8..63e807f4db7 100644 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/ThemeChooser.java +++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/ThemeChooser.java @@ -18,30 +18,22 @@ */ package org.apache.causeway.viewer.wicket.ui.components.widgets.themepicker; -import jakarta.inject.Inject; - -import org.apache.wicket.AttributeModifier; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.util.cookies.CookieUtils; -import org.apache.wicket.util.string.Strings; +import org.apache.causeway.commons.internal.base._StableValue; +import org.apache.causeway.commons.internal.base._Strings; +import org.apache.causeway.commons.internal.exceptions._Exceptions; +import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.viewer.wicket.ui.panels.PanelAbstract; import org.apache.causeway.viewer.wicket.ui.util.Wkt; -import lombok.Getter; - import de.agilecoders.wicket.core.Bootstrap; -import de.agilecoders.wicket.core.settings.ActiveThemeProvider; -import de.agilecoders.wicket.core.settings.IBootstrapSettings; import de.agilecoders.wicket.core.settings.ITheme; import de.agilecoders.wicket.core.settings.SessionThemeProvider; -import de.agilecoders.wicket.core.settings.SingleThemeProvider; import de.agilecoders.wicket.core.util.Attributes; -import de.agilecoders.wicket.themes.markup.html.bootstrap.BootstrapThemeTheme; -import de.agilecoders.wicket.themes.markup.html.bootswatch.BootswatchTheme; -import de.agilecoders.wicket.themes.markup.html.bootswatch.BootswatchThemeProvider; /** * A panel used as a Navbar item to change the application theme/skin @@ -51,13 +43,13 @@ public class ThemeChooser private static final long serialVersionUID = 1L; - @Inject @Getter transient private CausewayWicketThemeSupport themeSupport; - /** * The name of the cookie that stores the last user selection */ private static final String CAUSEWAY_THEME_COOKIE_NAME = "causeway.viewer.wicket.themes.selected"; + private final _StableValue themeSupport = new _StableValue<>(); + /** * Constructor * @@ -67,95 +59,81 @@ public ThemeChooser(final String id) { super(id); } + @Override + protected void onConfigure() { + super.onConfigure(); + setVisible(getWicketViewerSettings().themes().showChooser()); + } + @Override protected void onInitialize() { super.onInitialize(); + initializeActiveThemeFromCookie(); + Wkt.ajaxEnable(this); + } - if(getThemeSupport()==null) { - super.getMetaModelContext().injectServicesInto(this); - } + @Override + protected void onBeforeRender() { + super.onBeforeRender(); + buildGui(); + } - final ActiveThemeProvider activeThemeProvider = getActiveThemeProvider(); - if(activeThemeProvider.getClass() == SessionThemeProvider.class) { - initializeActiveThemeFromCookie(); - } else { - // if anything other than the default, then we do NOT initialize - // (on the assumption that it is a persistent store and we don't want to overwrite). - } + void buildGui() { + final String activeThemeName = getActiveTheme().name(); - Wkt.listViewAdd(this, "themes", getThemeSupport().getEnabledThemeNames(), item->{ + Wkt.listViewAdd(this, "themes", themeSupport().availableNames(), item->{ final String themeName = item.getModelObject(); - if (themeName.equals(getActiveThemeProvider().getActiveTheme().name())) { - item.add(AttributeModifier.append("class", "active")); - } - // use Ajax link because Link's url looks like /object:3 and this confuses the browser - Wkt.add(item, - Wkt.link("themeLink", target->{ - setActiveTheme(themeName); - saveActiveThemeToCookie(themeName); - target.add(getPage()); // repaint the whole page - }) - .setBody(Model.of(themeName))); + var link = Wkt.link("themeLink", target->{ + setActiveTheme(themeName); + target.add(getPage()); // repaint the whole page + }) + .setBody(Model.of(themeName)); + + if (themeName.equals(activeThemeName)) { + Wkt.cssAppend(link, "active"); + } + Wkt.add(item, link); }); - } - private void saveActiveThemeToCookie(final String themeName) { - CookieUtils cookieUtils = new CookieUtils(); - cookieUtils.save(CAUSEWAY_THEME_COOKIE_NAME, themeName); + @Override + protected void onComponentTag(final ComponentTag tag) { + super.onComponentTag(tag); + tag.setName("li"); + Attributes.addClass(tag, "dropdown"); } - private void initializeActiveThemeFromCookie() { - CookieUtils cookieUtils = new CookieUtils(); - String activeTheme = cookieUtils.load(CAUSEWAY_THEME_COOKIE_NAME); - if (!Strings.isEmpty(activeTheme)) { + // -- HELPER - var isAvailable = getThemeSupport().getThemeProvider().available().stream() - .anyMatch(theme->activeTheme.equals(theme.name())); - - if(isAvailable) { - setActiveTheme(activeTheme); - } - } - } - - private void setActiveTheme(final String activeTheme) { - IBootstrapSettings bootstrapSettings = Bootstrap.getSettings(); - ITheme theme = getThemeSupport().getThemeProvider().byName(activeTheme); - getActiveThemeProvider().setActiveTheme(theme); - if (theme instanceof BootstrapThemeTheme) { - bootstrapSettings.setThemeProvider(new SingleThemeProvider(theme)); - } else if (theme instanceof BootswatchTheme) { - bootstrapSettings.setThemeProvider(new BootswatchThemeProvider((BootswatchTheme) theme)); - /* - } else if (theme instanceof VegibitTheme) { - bootstrapSettings.setThemeProvider(new VegibitThemeProvider((VegibitTheme) theme)); - */ - } + private void initializeActiveThemeFromCookie() { + // legacy code - perhaps could be refactored to use CookieThemeProvider + if(!Bootstrap.getSettings().getActiveThemeProvider().getClass().equals(SessionThemeProvider.class)) + return; + _Strings.nonEmpty(new CookieUtils().load(CAUSEWAY_THEME_COOKIE_NAME)) + .filter(themeSupport().availableNames()::contains) + .ifPresent(this::setActiveTheme); } - private ActiveThemeProvider getActiveThemeProvider() { - return Bootstrap.getSettings().getActiveThemeProvider(); + private void setActiveTheme(final String themeName) { + ITheme theme = themeSupport().byName(themeName); + if(theme==null) + return; + new CookieUtils().save(CAUSEWAY_THEME_COOKIE_NAME, themeName); + Bootstrap.getSettings().getActiveThemeProvider().setActiveTheme(theme); } - @Override - protected void onComponentTag(final ComponentTag tag) { - super.onComponentTag(tag); - - tag.setName("li"); - Attributes.addClass(tag, "dropdown"); + private ITheme getActiveTheme() { + return Bootstrap.getSettings().getActiveThemeProvider().getActiveTheme(); } - @Override - protected void onConfigure() { - super.onConfigure(); - - boolean shouldShow = getWicketViewerSettings().themes().showChooser(); - setVisible(shouldShow); + private CausewayWicketThemeSupport themeSupport() { + return themeSupport.orElseSet(()->MetaModelContext.instance() + .flatMap(mmc->mmc.lookupService(CausewayWicketThemeSupport.class)) + .orElseThrow(()->_Exceptions.illegalState("no CausewayWicketThemeSupport found"))); } } diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/ThemeProviderComposite.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/ThemeProviderComposite.java deleted file mode 100644 index e87740917db..00000000000 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/ThemeProviderComposite.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.causeway.viewer.wicket.ui.components.widgets.themepicker; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.apache.wicket.util.string.Strings; - -import org.apache.causeway.commons.collections.Can; -import org.apache.causeway.commons.internal.collections._Lists; -import org.apache.causeway.commons.internal.collections._Maps; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import de.agilecoders.wicket.core.settings.ITheme; -import de.agilecoders.wicket.core.settings.ThemeProvider; - -/** - * - * @since 2.0 - * - */ -@RequiredArgsConstructor(staticName = "of") -@Slf4j -public class ThemeProviderComposite implements ThemeProvider { - - private final Can themeProviders; - - private ITheme defaultTheme; - private Map themesByName; - private List availableNames; - private List availableThemes; - - @Override - public ITheme byName(final String name) { - if (!Strings.isEmpty(name)) { - ensureInit(); - var theme = themesByName.get(name.toLowerCase()); - if(theme!=null) { - return theme; - } - } - - log.warn("'{}' theme not found amoung providers {} provinding {}, " - + "using default '{}' instead", - name, - themeProviders.toList(), - available().stream().map(ITheme::name).collect(Collectors.joining(", ")), - defaultTheme().name()); - - return defaultTheme(); - } - - @Override - public List available() { - ensureInit(); - return availableThemes; - } - - @Override - public ITheme defaultTheme() { - ensureInit(); - return defaultTheme; - } - - public List availableNames() { - ensureInit(); - return availableNames; - } - - // -- HELPER - - private void ensureInit() { - if(themesByName!=null) { - return; - } - themesByName = _Maps.newLinkedHashMap(); - themeProviders.forEach(themeProvider->{ - - if(defaultTheme==null) { - defaultTheme = themeProvider.defaultTheme(); - } - - themeProvider.available().forEach(theme->{ - themesByName.put(theme.name().toLowerCase(), theme); - }); - - }); - availableThemes = Collections.unmodifiableList(_Lists.newArrayList(themesByName.values())); - availableNames = Collections.unmodifiableList(_Lists.map(availableThemes, ITheme::name)); - } - -} diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/config/BootstrapInitWkt.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/config/BootstrapInitWkt.java index 44f7fb9b9c0..d4d972f8487 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/config/BootstrapInitWkt.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/config/BootstrapInitWkt.java @@ -53,11 +53,10 @@ public void renderHead(final IHeaderResponse response) { } }); - serviceRegistry.select(CausewayWicketThemeSupport.class) - .getFirst() - .ifPresent(themeSupport->{ - bsSettings.setThemeProvider(themeSupport.getThemeProvider()); - }); + serviceRegistry.lookupService(CausewayWicketThemeSupport.class) + .ifPresent(themeSupport->{ + bsSettings.setThemeProvider(themeSupport.getThemeProvider()); + }); } } From 77ec075db69b9345f7b3d85e35aad1f44e39c16c Mon Sep 17 00:00:00 2001 From: andi-huber Date: Wed, 27 May 2026 08:02:58 +0200 Subject: [PATCH 2/2] CAUSEWAY-4001: supports ThemeProvider specific resources --- .../wicket/ui/src/main/java/module-info.java | 2 +- .../CausewayWicketThemeSupport.java | 85 +++++++++++++------ .../widgets/themepicker/ThemeChooser.java | 7 ++ .../wicketapp/config/BootstrapInitWkt.java | 2 +- 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/viewers/wicket/ui/src/main/java/module-info.java b/viewers/wicket/ui/src/main/java/module-info.java index 6422c87935e..4f8171dcd87 100644 --- a/viewers/wicket/ui/src/main/java/module-info.java +++ b/viewers/wicket/ui/src/main/java/module-info.java @@ -143,7 +143,7 @@ requires spring.beans; requires spring.context; requires spring.core; - requires wicket.bootstrap.core; + requires transitive wicket.bootstrap.core; requires wicket.bootstrap.extensions; requires wicket.bootstrap.themes; requires de.agilecoders.wicket.webjars; diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/CausewayWicketThemeSupport.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/CausewayWicketThemeSupport.java index f83989f1f56..77fb2d8d8d7 100644 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/CausewayWicketThemeSupport.java +++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/CausewayWicketThemeSupport.java @@ -28,6 +28,7 @@ import jakarta.inject.Inject; import jakarta.inject.Named; +import org.apache.wicket.Component; import org.apache.wicket.util.string.Strings; import org.springframework.stereotype.Service; @@ -38,6 +39,7 @@ import lombok.extern.slf4j.Slf4j; +import de.agilecoders.wicket.core.Bootstrap; import de.agilecoders.wicket.core.settings.ITheme; import de.agilecoders.wicket.core.settings.NoopThemeProvider; import de.agilecoders.wicket.core.settings.ThemeProvider; @@ -50,23 +52,21 @@ @Slf4j public record CausewayWicketThemeSupport( ITheme defaultTheme, - Map themesByName) { + Map themeByNameLower, + Map providerByThemeNameLower) { @Inject public CausewayWicketThemeSupport(final CausewayConfiguration configuration, final ServiceRegistry serviceRegistry) { - this(configuration, collectThemes(configuration, serviceRegistry)); + this(Args.create(configuration, serviceRegistry)); } - private CausewayWicketThemeSupport(final CausewayConfiguration configuration, final Map themesByName) { - this( - Optional.ofNullable(themesByName.get(configuration.viewer().wicket().themes().initial())) - .orElseGet(()->new NoopThemeProvider().defaultTheme()), - themesByName); + private CausewayWicketThemeSupport(final Args args) { + this(args.defaultTheme, args.themeByNameLower, args.providerByThemeNameLower); } public ITheme byName(final String name) { if (!Strings.isEmpty(name)) { - var theme = themesByName.get(name.toLowerCase()); + var theme = themeByNameLower.get(name.toLowerCase()); if(theme!=null) return theme; } @@ -81,7 +81,7 @@ public ITheme byName(final String name) { } public List available() { - return themesByName.values() + return themeByNameLower.values() .stream() .toList(); } @@ -93,7 +93,7 @@ public List availableNames() { .toList(); } - public ThemeProvider getThemeProvider() { + public ThemeProvider compositeThemeProvider() { return new ThemeProvider() { @Override public ITheme defaultTheme() { return defaultTheme; @@ -107,23 +107,58 @@ public ThemeProvider getThemeProvider() { }; } + /** + * Required in order for ThemeProvider specific resources to be made available. + * + *

set during {@link Component#beforeRender()}, un-set during {@link Component#afterRender()} + * + * @see #unsetCustomThemeProvider() + */ + public void setCustomThemeProvider(final ITheme theme) { + Optional.ofNullable(providerByThemeNameLower.get(theme.name().toLowerCase())) + .ifPresent(Bootstrap.getSettings()::setThemeProvider); + } + + public void unsetCustomThemeProvider() { + Bootstrap.getSettings().setThemeProvider(compositeThemeProvider()); + } + // -- HELPER - private static Map collectThemes( - final CausewayConfiguration configuration, - final ServiceRegistry serviceRegistry) { - var enabledThemeNamesLowercase = _NullSafe.stream(configuration.viewer().wicket().themes().enabled()) - .map(String::toLowerCase) - .collect(Collectors.toCollection(HashSet::new)); - - var themesByName = new LinkedHashMap(); - serviceRegistry.select(ThemeProvider.class).stream() - .map(ThemeProvider::available) - .flatMap(List::stream) - .filter(theme->enabledThemeNamesLowercase.contains(theme.name().toLowerCase())) - .forEach(theme-> - themesByName.put(theme.name().toLowerCase(), theme)); - return themesByName; + /** + * Can be refactored/removed once we have flexible constructor bodies in Java. + */ + private record Args( + ITheme defaultTheme, + Map themeByNameLower, + Map providerByThemeNameLower) { + + static Args create( + final CausewayConfiguration configuration, + final ServiceRegistry serviceRegistry) { + + var enabledThemeNamesLowercase = _NullSafe.stream(configuration.viewer().wicket().themes().enabled()) + .map(String::toLowerCase) + .collect(Collectors.toCollection(HashSet::new)); + + var providerByThemeNameLower = new LinkedHashMap(); + var themeByNameLower = new LinkedHashMap(); + serviceRegistry.select(ThemeProvider.class) + .forEach(provider->{ + _NullSafe.stream(provider.available()) + .filter(theme->enabledThemeNamesLowercase.contains(theme.name().toLowerCase())) + .forEach(theme->{ + var themeKey = theme.name().toLowerCase(); + themeByNameLower.put(themeKey, theme); + providerByThemeNameLower.put(themeKey, provider); + }); + }); + + var defaultTheme = Optional.ofNullable(themeByNameLower.get(configuration.viewer().wicket().themes().initial())) + .orElseGet(()->new NoopThemeProvider().defaultTheme()); + + return new Args(defaultTheme, themeByNameLower, providerByThemeNameLower); + } } } diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/ThemeChooser.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/ThemeChooser.java index 63e807f4db7..9adea625d96 100644 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/ThemeChooser.java +++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/themepicker/ThemeChooser.java @@ -76,6 +76,13 @@ protected void onInitialize() { protected void onBeforeRender() { super.onBeforeRender(); buildGui(); + themeSupport().setCustomThemeProvider(getActiveTheme()); + } + + @Override + protected void onAfterRender() { + themeSupport().unsetCustomThemeProvider(); + super.onAfterRender(); } void buildGui() { diff --git a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/config/BootstrapInitWkt.java b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/config/BootstrapInitWkt.java index d4d972f8487..1526833d2fa 100644 --- a/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/config/BootstrapInitWkt.java +++ b/viewers/wicket/viewer/src/main/java/org/apache/causeway/viewer/wicket/viewer/wicketapp/config/BootstrapInitWkt.java @@ -55,7 +55,7 @@ public void renderHead(final IHeaderResponse response) { serviceRegistry.lookupService(CausewayWicketThemeSupport.class) .ifPresent(themeSupport->{ - bsSettings.setThemeProvider(themeSupport.getThemeProvider()); + bsSettings.setThemeProvider(themeSupport.compositeThemeProvider()); }); }