Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>com.flowingcode.vaadin.addons.demo</groupId>
<artifactId>commons-demo</artifactId>
<version>5.1.1-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>

<name>Commons Demo</name>
<description>Common classes for add-ons demo</description>
Expand Down
59 changes: 59 additions & 0 deletions src/main/java/com/flowingcode/vaadin/addons/demo/ColorScheme.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*-
* #%L
* Commons Demo
* %%
* Copyright (C) 2020 - 2026 Flowing Code
* %%
* Licensed 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.
* #L%
*/
package com.flowingcode.vaadin.addons.demo;

import lombok.Getter;
/*
* Copyright 2000-2025 Vaadin Ltd.
*
* Licensed 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.
*/
import lombok.RequiredArgsConstructor;

/**
* Enumeration of supported color scheme values.
* <p>
* These values correspond to the CSS color-scheme property values and control how the browser
* renders UI elements and how the application responds to system color scheme preferences.
*/
@RequiredArgsConstructor
public enum ColorScheme {
/**
* Light color scheme. The application will use a light theme regardless of system preferences.
*/
LIGHT("light"),

/**
* Dark color scheme. The application will use a dark theme regardless of system preferences.
*/
DARK("dark");

@Getter
private final String value;

}
131 changes: 112 additions & 19 deletions src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A layout for displaying a tabbed demo with source code integration.
* <p>
* This layout consists of a set of tabs for navigating between different demos,
* a content area for displaying the current demo, and a footer with controls
* for
* toggling the source code visibility, orientation, and theme.
* </p>
*/
@StyleSheet("context://frontend/styles/commons-demo/shared-styles.css")
@SuppressWarnings("serial")
public class TabbedDemo extends VerticalLayout implements RouterLayout {
Expand All @@ -67,6 +76,9 @@ public class TabbedDemo extends VerticalLayout implements RouterLayout {
private Button helperButton;
private DemoHelperViewer demoHelperViewer;

/**
* Constructs a new TabbedDemo instance.
*/
public TabbedDemo() {
demoHelperViewer = new DialogDemoHelperViewer();

Expand Down Expand Up @@ -94,8 +106,7 @@ public TabbedDemo() {
themeCB.addClassName("smallcheckbox");
themeCB.addValueChangeListener(cb -> {
boolean useDarkTheme = themeCB.getValue();
String theme = useDarkTheme ? "dark" : "";
applyThemeAttribute(getElement(), theme);
setColorScheme(this, useDarkTheme ? ColorScheme.DARK : ColorScheme.LIGHT);
});
footer = new HorizontalLayout();
footer.setWidthFull();
Expand Down Expand Up @@ -125,6 +136,7 @@ public TabbedDemo() {
*
* @param clazz the class of routed demo view component
* @param label the demo name (tab label)
* @throws IllegalArgumentException if the class is not annotated with {@link Route}
*/
public void addDemo(Class<? extends Component> clazz, String label) {
if (!clazz.isAnnotationPresent(Route.class)) {
Expand All @@ -143,6 +155,8 @@ private void updateVisibility() {
/**
* Sets the autovisibility mode. When autovisibility is enabled, the tabs component is hidden
* unless it contains two or more tabs.
*
* @param autoVisibility {@code true} to enable autovisibility, {@code false} to disable it
*/
public void setAutoVisibility(boolean autoVisibility) {
this.autoVisibility = autoVisibility;
Expand Down Expand Up @@ -201,11 +215,11 @@ public void showRouterLayoutContent(HasElement content) {
demo.getElement().getStyle().set("height", "100%");
setupDemoHelperButton(content.getClass());
}

updateFooterButtonsVisibility();
getElement().insertChild(1, content.getElement());

applyThemeAttribute(getElement(), getThemeAttribute());
setColorScheme(this, getColorScheme());
}

private static SourceUrlResolver resolver = null;
Expand All @@ -216,7 +230,7 @@ public void showRouterLayoutContent(HasElement content) {
*
* @param resolver The {@code SourceUrlResolver} to be used. Must not be {@code null}.
* @throws IllegalStateException if a resolver has already been set.
* @throws NullPointerException if the provided {@code resolver} is {@code null}.
* @throws NullPointerException if the provided {@code resolver} is {@code null}.
*/
public static void configureSourceUrlResolver(@NonNull SourceUrlResolver resolver) {
if (TabbedDemo.resolver != null) {
Expand All @@ -235,17 +249,17 @@ private static SourceUrlResolver getResolver() {
private Optional<SourceCodeTab> createSourceCodeTab(Class<?> annotatedClass, DemoSource annotation) {
String url = getResolver().resolveURL(this, annotatedClass, annotation).orElse(null);

if (url==null) {
if (url == null) {
return Optional.empty();
}

SourceCodeTab.SourceCodeTabBuilder builder = SourceCodeTab.builder();
builder.url(url);

if (!annotation.caption().equals(DemoSource.DEFAULT_VALUE)) {
builder.caption(annotation.caption());
}

if (!annotation.language().equals(DemoSource.DEFAULT_VALUE)) {
builder.language(annotation.language());
}
Expand All @@ -255,15 +269,21 @@ private Optional<SourceCodeTab> createSourceCodeTab(Class<?> annotatedClass, Dem
}

builder.sourcePosition(annotation.sourcePosition());

return Optional.of(builder.build());
}


/**
* Looks up the GitHub branch name associated with the given TabbedDemo class.
*
* @param clazz the TabbedDemo class to inspect
* @return the GitHub branch name, or "master" if the annotation is not found
*/
public static String lookupGithubBranch(Class<? extends TabbedDemo> clazz) {
GithubBranch branch = clazz.getAnnotation(GithubBranch.class);
if (branch == null) {
Package pkg = clazz.getPackage();
if (pkg!=null) {
if (pkg != null) {
branch = pkg.getAnnotation(GithubBranch.class);
}
}
Expand All @@ -287,11 +307,19 @@ private void updateSplitterPosition() {
}
}

/**
* Sets the visibility of the source code.
*
* @param visible {@code true} to make the source code visible, {@code false} otherwise
*/
public void setSourceVisible(boolean visible) {
codeCB.setValue(visible);
codePositionCB.setVisible(visible);
}

/**
* Toggles the position of the source code relative to the demo content.
*/
public void toggleSourcePosition() {
if (currentLayout != null) {
currentLayout.toggleSourcePosition();
Expand All @@ -310,10 +338,20 @@ private void toggleSplitterOrientation() {
setOrientation(splitOrientation);
}

/**
* Returns the current orientation of the split layout.
*
* @return the current orientation
*/
public Orientation getOrientation() {
return currentLayout.getOrientation();
}

/**
* Sets the orientation of the split layout.
*
* @param orientation the new orientation
*/
public void setOrientation(Orientation orientation) {
splitOrientation = orientation;
if (currentLayout != null) {
Expand All @@ -323,16 +361,62 @@ public void setOrientation(Orientation orientation) {
orientationCB.setValue(Orientation.HORIZONTAL.equals(orientation));
}

private static final String THEME_NAME = TabbedDemo.class.getName() + "#THEME_NAME";

/**
* Returns the theme attribute value.
* <p>
* The "theme attribute" is either an empty string (light) or "dark".
* </p>
*
* @deprecated Use {@link #getColorScheme()}
* @return the theme attribute value
*/
@Deprecated
public static String getThemeAttribute() {
return (String) Optional.ofNullable(VaadinSession.getCurrent().getAttribute(THEME_NAME))
.orElse("");
ColorScheme scheme = getColorScheme();
return scheme == ColorScheme.LIGHT ? "" : scheme.getValue();
}

/**
* Returns the current color scheme.
*
* @return the current color scheme
*/
public static ColorScheme getColorScheme() {
return Optional.ofNullable(VaadinSession.getCurrent().getAttribute(ColorScheme.class))
.orElse(ColorScheme.LIGHT);
}

/**
* Applies the theme attribute to the given element.
* <p>
* The "theme attribute" is either an empty string (light) or "dark".
* </p>
*
* @param element the element to apply the theme to
* @param theme the theme attribute value
* @deprecated Use {@link #setColorScheme(Component, ColorScheme)}
*/
@Deprecated
public static void applyThemeAttribute(Element element, String theme) {
VaadinSession.getCurrent().setAttribute(THEME_NAME, theme);
Component c = element.getComponent().get();
if (theme.isEmpty()) {
setColorScheme(c, ColorScheme.LIGHT);
} else if (theme.equals("dark")) {
setColorScheme(c, ColorScheme.DARK);
}
}

/**
* Sets the color scheme for the given component.
*
* @param component the component to apply the color scheme to
* @param colorScheme the color scheme to apply
*/
public static void setColorScheme(Component component, @NonNull ColorScheme colorScheme) {
VaadinSession.getCurrent().setAttribute(ColorScheme.class, colorScheme);
String theme = colorScheme.getValue();

Element element = component.getElement();
String script;
if (element.getTag().equalsIgnoreCase("iframe")) {
script = "let e = this.contentWindow.document.documentElement;";
Expand All @@ -347,8 +431,7 @@ public static void applyThemeAttribute(Element element, String theme) {

element.executeJs(script, theme);

Component c = element.getComponent().get();
collectThemeChangeObservers(c).forEach(observer -> observer.onThemeChange(theme));
collectThemeChangeObservers(component).forEach(observer -> observer.onThemeChange(theme));
}

private static Stream<ThemeChangeObserver> collectThemeChangeObservers(Component c) {
Expand All @@ -365,6 +448,11 @@ private void updateFooterButtonsVisibility() {
codePositionCB.setVisible(hasSourceCode);
}

/**
* Adds a listener for {@link TabbedDemoSourceEvent}.
*
* @param listener the listener to add
*/
public void addTabbedDemoSourceListener(ComponentEventListener<TabbedDemoSourceEvent> listener) {
ComponentUtil.addListener(this, TabbedDemoSourceEvent.class, listener);
listener.onComponentEvent(new TabbedDemoSourceEvent(this, currentLayout != null));
Expand Down Expand Up @@ -393,6 +481,11 @@ private void adjustSplitOrientation(boolean portraitOrientation) {
setOrientation(splitOrientation);
}

/**
* Sets the {@link DemoHelperViewer} to be used for displaying demo helpers.
*
* @param demoHelperViewer the viewer to set
*/
public void setDemoHelperViewer(DemoHelperViewer demoHelperViewer) {
this.demoHelperViewer =
Objects.requireNonNull(demoHelperViewer, "Demo helper viewer cannot be null");
Expand All @@ -411,7 +504,7 @@ private void setupDemoHelperButton(Class<?> helperClass) {
.addClickListener(e -> demoHelperViewer.show(demoHelperRenderer.helperContent()));
add(helperButton);
} catch (Exception e) {
logger.error("Error creating an instance",e);
logger.error("Error creating an instance", e);
}
}
}
Expand Down