diff --git a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/ButtongridRenderer.java b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/ButtongridRenderer.java new file mode 100644 index 0000000000..efab75856a --- /dev/null +++ b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/ButtongridRenderer.java @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.ui.basic.internal.render; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.emf.common.util.ECollections; +import org.eclipse.emf.common.util.EList; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.model.sitemap.sitemap.Button; +import org.openhab.core.model.sitemap.sitemap.Buttongrid; +import org.openhab.core.model.sitemap.sitemap.Widget; +import org.openhab.core.ui.items.ItemUIRegistry; +import org.openhab.ui.basic.internal.WebAppConfig; +import org.openhab.ui.basic.render.RenderException; +import org.openhab.ui.basic.render.WidgetRenderer; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an implementation of the {@link WidgetRenderer} interface, which + * can produce HTML code for Buttongrid widgets. + * + * @author Laurent Garnier - Initial contribution + */ +@Component(service = WidgetRenderer.class) +@NonNullByDefault +public class ButtongridRenderer extends AbstractWidgetRenderer { + + private final Logger logger = LoggerFactory.getLogger(ButtongridRenderer.class); + + @Activate + public ButtongridRenderer(final BundleContext bundleContext, final @Reference TranslationProvider i18nProvider, + final @Reference ItemUIRegistry itemUIRegistry, final @Reference LocaleProvider localeProvider) { + super(bundleContext, i18nProvider, itemUIRegistry, localeProvider); + } + + @Override + public boolean canRender(Widget w) { + return w instanceof Buttongrid; + } + + @Override + public EList renderWidget(Widget w, StringBuilder sb, String sitemap) throws RenderException { + Buttongrid grid = (Buttongrid) w; + + Map> rows = new HashMap<>(); + + int maxColumn = 0; + int mawRow = 0; + for (Button button : grid.getButtons()) { + int row = button.getRow(); + int column = button.getColumn(); + if (row < 1 || column < 1) { + logger.warn("Invalid row or column number; button at position {}:{} is ignored", row, column); + continue; + } + if (row > mawRow) { + mawRow = row; + } + if (column > maxColumn) { + maxColumn = column; + } + + Map columns = rows.get(row); + if (columns == null) { + columns = new HashMap<>(); + rows.put(row, columns); + } + columns.put(column, button); + } + + if (mawRow > 50 || maxColumn > 12) { + logger.warn("The button grid is too big ({},{})", mawRow, maxColumn); + return ECollections.emptyEList(); + } + + String snippet = getSnippet("buttongrid"); + + boolean showHeaderRow = grid.getLabel() != null; + snippet = snippet.replace("%header_visibility_class%", + showHeaderRow ? "%visibility_class%" : "mdl-form__row--hidden"); + snippet = snippet.replace("%header_row%", Boolean.valueOf(showHeaderRow).toString()); + + snippet = preprocessSnippet(snippet, w, true); + + // Process the color tags + snippet = processColor(w, snippet); + + StringBuilder buttons = new StringBuilder(); + for (int row = 1; row <= mawRow; row++) { + buildRow(grid.getItem(), maxColumn, rows.get(row), buttons); + } + snippet = snippet.replace("%buttons%", buttons.toString()); + + sb.append(snippet); + return ECollections.emptyEList(); + } + + private void buildRow(String item, int columns, @Nullable Map buttonsInRow, StringBuilder builder) + throws RenderException { + // Add extra cells to fill the row + // Try to center the grid at best with one extra cell at beginning of row and one at end of row + int extraCellSizeDesktop = 12 % columns; + int extraCellSizeTablet = columns > 8 ? 0 : 8 % columns; + int column = columns + 1; + // Extra cell at beginning + if (extraCellSizeDesktop > 0) { + buildEmptyCell((extraCellSizeDesktop / 2) == 0, Math.max(1, extraCellSizeDesktop / 2), + (column > 8) || ((extraCellSizeTablet / 2) == 0), Math.max(1, extraCellSizeTablet / 2), true, 1, + builder); + } else if (extraCellSizeTablet > 0 && columns < 8) { + buildEmptyCell(true, 1, (extraCellSizeTablet / 2) == 0, Math.max(1, extraCellSizeTablet / 2), true, 1, + builder); + } + + // Match the grid to a mdl-grid + int sizeDessktop = Math.max(1, 12 / columns); + int sizeTablet = Math.max(1, 8 / columns); + int sizePhone = Math.max(1, 4 / columns); + for (int col = 1; col <= columns; col++) { + Button button = buttonsInRow == null ? null : buttonsInRow.get(col); + if (button != null) { + String buttonHtml = buildButton(item, button.getLabel(), button.getCmd(), button.getIcon()); + buildCell(false, sizeDessktop, col > 8, sizeTablet, col > 4, sizePhone, buttonHtml, builder); + } else { + buildEmptyCell(false, sizeDessktop, col > 8, sizeTablet, col > 4, sizePhone, builder); + } + } + + // Extra cell at end + if (extraCellSizeDesktop > 0) { + buildEmptyCell(false, extraCellSizeDesktop / 2 + extraCellSizeDesktop % 2, column > 8, + Math.max(1, extraCellSizeTablet / 2 + extraCellSizeTablet % 2), column > 4, 1, builder); + } else if (extraCellSizeTablet > 0 && columns < 8) { + buildEmptyCell(true, 1, false, extraCellSizeTablet / 2 + extraCellSizeTablet % 2, column > 4, 1, builder); + } + } + + private void buildEmptyCell(boolean hideDesktop, int sizeDessktop, boolean hideTablet, int sizeTablet, + boolean hidePhone, int sizePhone, StringBuilder builder) throws RenderException { + buildCell(hideDesktop, sizeDessktop, hideTablet, sizeTablet, hidePhone, sizePhone, "", builder); + } + + private void buildCell(boolean hideDesktop, int sizeDessktop, boolean hideTablet, int sizeTablet, boolean hidePhone, + int sizePhone, String buttonHtml, StringBuilder builder) throws RenderException { + String divClass = ""; + if (hideDesktop) { + divClass += " mdl-cell--hide-desktop"; + } + if (hideTablet) { + divClass += " mdl-cell--hide-tablet"; + } + if (hidePhone) { + divClass += " mdl-cell--hide-phone"; + } + String buttonDiv = getSnippet("buttoncell"); + buttonDiv = buttonDiv.replace("%size_desktop%", String.valueOf(sizeDessktop)); + buttonDiv = buttonDiv.replace("%size_tablet%", String.valueOf(sizeTablet)); + buttonDiv = buttonDiv.replace("%size_phone%", String.valueOf(sizePhone)); + buttonDiv = buttonDiv.replace("%class%", divClass); + buttonDiv = buttonDiv.replace("%button%", buttonHtml); + builder.append(buttonDiv); + } + + private String buildButton(String item, @Nullable String lab, String cmd, @Nullable String icon) + throws RenderException { + String button = getSnippet("button"); + + String command = cmd; + String label = lab == null ? cmd : lab; + + button = button.replace("%item%", item); + button = button.replace("%cmd%", escapeHtml(command)); + String buttonClass = "buttongrid-button"; + String style = ""; + if (icon == null || !config.isIconsEnabled()) { + button = button.replace("%label%", escapeHtml(label)); + button = button.replace("%icon_snippet%", ""); + } else { + button = button.replace("%label%", ""); + button = preprocessIcon(button, icon, true); + buttonClass += " mdl-button-icon"; + switch (config.getTheme()) { + case WebAppConfig.THEME_NAME_BRIGHT: + style = "style=\"color-scheme: light\""; + break; + case WebAppConfig.THEME_NAME_DARK: + style = "style=\"color-scheme: dark\""; + break; + default: + break; + } + } + button = button.replace("%buttonstyle%", style); + button = button.replace("%class%", buttonClass); + + return button; + } +} diff --git a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/SwitchRenderer.java b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/SwitchRenderer.java index e182df1c99..1a17c270d0 100644 --- a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/SwitchRenderer.java +++ b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/SwitchRenderer.java @@ -138,7 +138,6 @@ public EList renderWidget(Widget w, StringBuilder sb, String sitemap) th } } snippet = snippet.replace("%buttons%", buttons.toString()); - snippet = snippet.replace("%count%", Integer.toString(nbButtons)); } // Process the color tags diff --git a/bundles/org.openhab.ui.basic/src/main/resources/snippets/buttoncell.html b/bundles/org.openhab.ui.basic/src/main/resources/snippets/buttoncell.html new file mode 100644 index 0000000000..9f008b572e --- /dev/null +++ b/bundles/org.openhab.ui.basic/src/main/resources/snippets/buttoncell.html @@ -0,0 +1,3 @@ +
+ %button% +
diff --git a/bundles/org.openhab.ui.basic/src/main/resources/snippets/buttongrid.html b/bundles/org.openhab.ui.basic/src/main/resources/snippets/buttongrid.html new file mode 100644 index 0000000000..bf66c46833 --- /dev/null +++ b/bundles/org.openhab.ui.basic/src/main/resources/snippets/buttongrid.html @@ -0,0 +1,20 @@ +
+ + %icon_snippet% + + + %label% + +
+
+
+ %buttons% +
+
diff --git a/bundles/org.openhab.ui.basic/src/main/resources/snippets/buttons.html b/bundles/org.openhab.ui.basic/src/main/resources/snippets/buttons.html index 198a56e386..4b1b93c2c5 100644 --- a/bundles/org.openhab.ui.basic/src/main/resources/snippets/buttons.html +++ b/bundles/org.openhab.ui.basic/src/main/resources/snippets/buttons.html @@ -13,7 +13,6 @@ data-control-type="buttons" data-item="%item%" data-has-value="%has_value%" - data-count="%count%" data-widget-id="%widget_id%" data-icon-with-state="%icon_with_state%" > diff --git a/bundles/org.openhab.ui.basic/web-src/_layout.scss b/bundles/org.openhab.ui.basic/web-src/_layout.scss index b4dd2672ff..d55ee8fbe5 100644 --- a/bundles/org.openhab.ui.basic/web-src/_layout.scss +++ b/bundles/org.openhab.ui.basic/web-src/_layout.scss @@ -115,6 +115,13 @@ display: none; } } + &__row-buttongrid { + margin: 0; + border-bottom: 1px solid $item-separator-color; + &:last-child { + border: none; + } + } &__image { &.mdl-form__control { padding-left: 0; @@ -241,6 +248,13 @@ left: 0; } } + .buttongrid-cell { + height: 36px; + .buttongrid-button { + min-width: 100%; + text-transform: none; + } + } .mdl-button, .mdl-button:focus { box-shadow: none; @@ -417,6 +431,9 @@ &__buttons { padding-top: 2px; } + &__buttongrid { + padding: 0; + } &--no-label { .mdl-form__title { display: none; diff --git a/bundles/org.openhab.ui.basic/web-src/_theming.scss b/bundles/org.openhab.ui.basic/web-src/_theming.scss index 95451647d2..d40f818626 100644 --- a/bundles/org.openhab.ui.basic/web-src/_theming.scss +++ b/bundles/org.openhab.ui.basic/web-src/_theming.scss @@ -82,7 +82,8 @@ body { color: var(--container-text-color, #616161) !important; } -.mdl-form__row { +.mdl-form__row, +.mdl-form__row-buttongrid { border-bottom: 1px solid #ccc; border-bottom: 1px solid var(--border-color, #ccc); color: #616161 !important; diff --git a/bundles/org.openhab.ui.basic/web-src/smarthome.js b/bundles/org.openhab.ui.basic/web-src/smarthome.js index 2ed4674834..c070253600 100644 --- a/bundles/org.openhab.ui.basic/web-src/smarthome.js +++ b/bundles/org.openhab.ui.basic/web-src/smarthome.js @@ -1009,9 +1009,9 @@ var _t = this; + _t.ignoreState = _t.parentNode.getAttribute("data-ignore-state") === "true"; _t.hasValue = _t.parentNode.getAttribute("data-has-value") === "true"; _t.value = _t.parentNode.parentNode.querySelector(o.formValue); - _t.count = _t.parentNode.getAttribute("data-count") * 1; _t.suppressUpdateButtons = false; _t.reset = function() { _t.buttons.forEach(function(button) { @@ -1024,8 +1024,10 @@ var value = this.getAttribute("data-value") + ""; - _t.reset(); - this.classList.add(o.buttonActiveClass); + if (!_t.ignoreState) { + _t.reset(); + this.classList.add(o.buttonActiveClass); + } _t.parentNode.dispatchEvent(createEvent( "control-change", { @@ -1037,6 +1039,10 @@ _t.valueMap = {}; _t.buttons = [].slice.call(_t.parentNode.querySelectorAll(o.controlButton)); _t.setValuePrivate = function(value, itemState) { + if (_t.ignoreState) { + return; + } + if (_t.hasValue) { _t.value.innerHTML = value; } @@ -1056,7 +1062,9 @@ }; _t.setValueColor = function(color) { - _t.value.style.color = color; + if (_t.hasValue) { + _t.value.style.color = color; + } }; _t.buttons.forEach.call(_t.buttons, function(button) {