[BasicUI] Add a new header line for image, chart and video elements. (#2010)

Header line for video element contains icon and label.

Header line for image element contains icon, label and a button to
switch between no upscale and upscale of the image. Header line for
chart element contains icon, label and 4 buttons:
 - one button to show or hide the legend
 - one button to change the time range
 - one button to switch between no upscale and upscale of the chart
 - one button to refresh the chart

Fix handling of iconcolor and labelcolor parameters for mapview and
webview elements.

For image and chart elements, the header line is always present so that
user has an access to its buttons.
For video, mapview and webview elements, if the label is empty, the
header line is hidden.

For chart and image elements, there is now no upscale applied by default
(tablet/phone devices) but a button allows upscaling.

Closes #1939
Fixes #1367
Also related to #1930

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
pull/1998/head^2
lolodomo 2023-09-09 12:31:03 +02:00 committed by GitHub
parent 1ee7a2f972
commit 08a2d80db0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 365 additions and 30 deletions

View File

@ -15,10 +15,12 @@ package org.openhab.ui.basic.internal.render;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
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.items.GroupItem;
@ -91,7 +93,9 @@ public class ChartRenderer extends AbstractWidgetRenderer {
}
// if legend parameter is given, add corresponding GET parameter
boolean legend = item instanceof GroupItem && !forceAsItem;
if (chart.getLegend() != null) {
legend = chart.getLegend();
if (chart.getLegend()) {
chartUrl += "&legend=true";
} else {
@ -123,7 +127,10 @@ public class ChartRenderer extends AbstractWidgetRenderer {
}
String snippet = getSnippet("chart");
snippet = preprocessSnippet(snippet, w);
snippet = preprocessSnippet(snippet, w, true);
// Process the color tags
snippet = processColor(w, snippet);
if (chart.getRefresh() > 0) {
snippet = snippet.replace("%update_interval%", Integer.toString(chart.getRefresh()));
@ -136,6 +143,18 @@ public class ChartRenderer extends AbstractWidgetRenderer {
snippet = snippet.replace("%valid_url%", "true");
snippet = snippet.replace("%ignore_refresh%", ignoreRefresh ? "true" : "false");
snippet = snippet.replace("%url%", url);
snippet = snippet.replace("%legend%", Boolean.valueOf(legend).toString());
List<List<String>> periods = List.of(List.of("Last hour", "h"), List.of("Last 4 hours", "4h"),
List.of("Last 8 hours", "8h"), List.of("Last 12 hours", "12h"), List.of("Last day", "D"),
List.of("Last 2 days", "2D"), List.of("Last 3 days", "3D"), List.of("Last week", "W"),
List.of("Last 2 weeks", "2W"), List.of("Last month", "M"), List.of("Last 2 months", "2M"),
List.of("Last 4 months", "4M"), List.of("Last year", "Y"));
StringBuilder rowSB = new StringBuilder();
for (List<String> period : periods) {
buildRow(chart, period.get(0), period.get(1), chart.getPeriod(), rowSB);
}
snippet = snippet.replace("%period_rows%", rowSB.toString());
sb.append(snippet);
} catch (ItemNotFoundException e) {
@ -143,4 +162,24 @@ public class ChartRenderer extends AbstractWidgetRenderer {
}
return ECollections.emptyEList();
}
private void buildRow(Chart w, @Nullable String lab, String cmd, String current, StringBuilder rowSB)
throws RenderException {
String rowSnippet = getSnippet("selection_row");
String command = cmd;
String label = lab == null ? cmd : lab;
rowSnippet = rowSnippet.replace("%item%", w.getItem() != null ? w.getItem() : "");
rowSnippet = rowSnippet.replace("%cmd%", escapeHtml(command));
rowSnippet = rowSnippet.replace("%label%", escapeHtml(label));
if (command.equals(current)) {
rowSnippet = rowSnippet.replace("%checked%", "checked=\"true\"");
} else {
rowSnippet = rowSnippet.replace("%checked%", "");
}
rowSB.append(rowSnippet);
}
}

View File

@ -69,7 +69,10 @@ public class ImageRenderer extends AbstractWidgetRenderer {
String widgetId = itemUIRegistry.getWidgetId(w);
snippet = snippet.replace("%id%", widgetId);
snippet = preprocessSnippet(snippet, w);
snippet = preprocessSnippet(snippet, w, true);
// Process the color tags
snippet = processColor(w, snippet);
boolean validUrl = isValidURL(image.getUrl());
String proxiedUrl = "../proxy?sitemap=" + sitemap + "&amp;widgetId=" + widgetId;

View File

@ -57,7 +57,14 @@ public class MapviewRenderer extends AbstractWidgetRenderer {
public EList<Widget> renderWidget(Widget w, StringBuilder sb, String sitemap) throws RenderException {
Mapview mapview = (Mapview) w;
String snippet = getSnippet("mapview");
boolean showHeaderRow = !getLabel(w).isEmpty();
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, mapview, true);
// Process the color tags
snippet = processColor(w, snippet);

View File

@ -66,7 +66,16 @@ public class VideoRenderer extends AbstractWidgetRenderer {
&& videoWidget.getEncoding().toLowerCase().contains("mjpeg")) ? "image" : "video";
snippet = getSnippet(snippetName);
snippet = preprocessSnippet(snippet, w);
boolean showHeaderRow = !getLabel(w).isEmpty();
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);
State state = itemUIRegistry.getState(w);
String url;

View File

@ -52,7 +52,14 @@ public class WebviewRenderer extends AbstractWidgetRenderer {
public EList<Widget> renderWidget(Widget w, StringBuilder sb, String sitemap) throws RenderException {
Webview webview = (Webview) w;
String snippet = getSnippet("webview");
boolean showHeaderRow = !getLabel(w).isEmpty();
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, webview, true);
// Process the color tags
snippet = processColor(w, snippet);

View File

@ -1,3 +1,38 @@
<div class="mdl-form__row mdl-form__row--header mdl-cell mdl-cell--12-col %visibility_class%">
<span %iconstyle% class="mdl-form__icon">
%icon_snippet%
</span>
<span %labelstyle% class="mdl-form__label">
%label%
</span>
<div class="mdl-form__header">
<button id="button1-%widget_id%" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect chart-legend-button">
<!-- legend_toggle -->
<i class="material-icons">&#xF11B;</i>
</button>
<spam class="mdl-tooltip mdl-tooltip--top" for="button1-%widget_id%">Show or hde legend</spam>
<button id="button2-%widget_id%" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect chart-period-button">
<!-- more_time -->
<i class="material-icons">&#xEA5D;</i>
</button>
<spam class="mdl-tooltip mdl-tooltip--top" for="button2-%widget_id%">Choose time frame</spam>
<script type="text/html" class="mdl-form__header-rows">
<div class="mdl-radio__group">
%period_rows%
</div>
</script>
<button id="button3-%widget_id%" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect image-upscale-button">
<!-- fullscreen -->
<i class="material-icons">&#xE5D0;</i>
</button>
<spam class="mdl-tooltip mdl-tooltip--top" for="button3-%widget_id%">Upscale chart or not</spam>
<button id="button4-%widget_id%" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect chart-refresh-button">
<!-- refresh -->
<i class="material-icons">&#xE5D5;</i>
</button>
<spam class="mdl-tooltip mdl-tooltip--top" for="button4-%widget_id%">Refresh chart</spam>
</div>
</div>
<div class="mdl-form__row mdl-form__row--height-auto mdl-cell mdl-cell--12-col %visibility_class%">
<div
class="mdl-form__control mdl-form__image"
@ -7,6 +42,8 @@
data-proxied-url="%proxied_url%"
data-valid-url="%valid_url%"
data-ignore-refresh="%ignore_refresh%"
data-header-row="true"
data-legend="%legend%"
>
<img src="%url%" />
</div>

View File

@ -1,3 +1,18 @@
<div class="mdl-form__row mdl-form__row--header mdl-cell mdl-cell--12-col %visibility_class%">
<span %iconstyle% class="mdl-form__icon">
%icon_snippet%
</span>
<span %labelstyle% class="mdl-form__label">
%label%
</span>
<div class="mdl-form__header">
<button id="button-%widget_id%" class="mdl-button mdl-button--raised mdl-js-button mdl-js-ripple-effect image-upscale-button">
<!-- fullscreen -->
<i class="material-icons">&#xE5D0;</i>
</button>
<spam class="mdl-tooltip mdl-tooltip--top" for="button-%widget_id%">Upscale image or not</spam>
</div>
</div>
<div class="mdl-form__row mdl-form__row--height-auto mdl-cell mdl-cell--12-col %visibility_class%">
<div
class="mdl-form__control mdl-form__image"
@ -8,6 +23,7 @@
data-proxied-url="%proxied_url%"
data-valid-url="%valid_url%"
data-ignore-refresh="%ignore_refresh%"
data-header-row="true"
>
<img src="%url%" />
</div>

View File

@ -1,10 +1,10 @@
<div class="mdl-form__row mdl-cell mdl-cell--12-col %visibility_class%">
<span %iconstyle% class="mdl-form__icon">
%icon_snippet%
</span>
<span %labelstyle% class="mdl-form__label">
%label%
</span>
<div class="mdl-form__row mdl-form__row--header mdl-cell mdl-cell--12-col %header_visibility_class%">
<span %iconstyle% class="mdl-form__icon">
%icon_snippet%
</span>
<span %labelstyle% class="mdl-form__label">
%label%
</span>
</div>
<div class="mdl-form__row mdl-form__row--height-auto mdl-cell mdl-cell--12-col %visibility_class%">
<div
@ -13,6 +13,7 @@
data-widget-id="%widget_id%"
data-map-url="%map_url%"
data-map-zoom="%map_zoom%"
data-header-row="%header_row%"
>
<iframe src="%url%" height="%height%"></iframe>
</div>

View File

@ -1,8 +1,17 @@
<div class="mdl-form__row mdl-form__row--header mdl-cell mdl-cell--12-col %header_visibility_class%">
<span %iconstyle% class="mdl-form__icon">
%icon_snippet%
</span>
<span %labelstyle% class="mdl-form__label">
%label%
</span>
</div>
<div class="mdl-form__row mdl-form__row--height-auto mdl-cell mdl-cell--12-col %visibility_class%">
<div
class="mdl-form__control mdl-form__video"
data-control-type="video"
data-widget-id="%widget_id%"
data-header-row="%header_row%"
>
<video autoplay controls src="%url%" %media_type%></video>
</div>

View File

@ -1,16 +1,17 @@
<div class="mdl-form__row mdl-cell mdl-cell--12-col %visibility_class%">
<span %iconstyle% class="mdl-form__icon">
%icon_snippet%
</span>
<span %labelstyle% class="mdl-form__label">
%label%
</span>
<div class="mdl-form__row mdl-form__row--header mdl-cell mdl-cell--12-col %header_visibility_class%">
<span %iconstyle% class="mdl-form__icon">
%icon_snippet%
</span>
<span %labelstyle% class="mdl-form__label">
%label%
</span>
</div>
<div class="mdl-form__row mdl-form__row--height-auto mdl-cell mdl-cell--12-col %visibility_class%">
<div
class="mdl-form__control mdl-form__webview"
data-control-type="webview"
data-widget-id="%widget_id%"
data-header-row="%header_row%"
>
<iframe src="%url%" height="%height%"></iframe>
</div>

View File

@ -70,6 +70,9 @@
@include align-items-2011(center);
white-space: nowrap;
height: $form-row-desktop-height;
html.ui-layout-condensed & {
height: $form-row-desktop-height-condensed;
}
box-sizing: content-box;
padding: 4px 0;
margin: 0 $mdl-grid-spacing;
@ -79,7 +82,7 @@
padding: 4px $mdl-grid-spacing;
margin: 0;
html.ui-layout-condensed & {
height: $form-row-desktop-height-condensed;
height: $form-row-mobile-height-condensed;
}
}
@media screen and (min-width: $layout-tablet-size-threshold) {
@ -98,12 +101,19 @@
height: auto;
}
}
&--header {
border-bottom: none;
padding-bottom: 0;
padding-left: 48px;
padding-right: 48px;
@media screen and (max-width: $layout-tablet-size-threshold) {
padding-left: $mdl-grid-spacing + 16px;
padding-right: $mdl-grid-spacing + 16px;
}
}
&--hidden {
display: none;
}
html.ui-layout-condensed & {
height: $form-row-desktop-height-condensed;
}
}
&__image {
&.mdl-form__control {
@ -114,11 +124,7 @@
max-width: 100%;
}
}
@media screen and (max-width: $layout-tablet-size-threshold) {
&.mdl-form__control {
padding-left: 0;
padding-right: 0;
}
&-upscale {
width: 100%;
img {
height: auto;
@ -264,6 +270,22 @@
padding-top: 6px;
}
}
&__header {
padding-left: 8px;
padding-right: $form-row-desktop-padding;
@media screen and (max-width: $layout-tablet-size-threshold) {
padding-right: $form-row-mobile-padding;
}
.mdl-button {
box-shadow: none;
-webkit-box-shadow: none;
min-width: 0;
padding-top: 6px;
}
&-rows {
display: none;
}
}
&__group {
padding-top: 4px;
}

View File

@ -84,6 +84,10 @@ body {
color: var(--container-text-color, #616161) !important;
}
.mdl-form__row--header {
border-bottom: none;
}
.mdl-switch .mdl-switch__track {
background: rgba(0,0,0,.26);
background: var(--switch-off-track-bg, rgba(0,0,0,.26));

View File

@ -358,10 +358,19 @@
}
_t.item = _t.parentNode.getAttribute(o.itemAttribute);
_t.id = _t.parentNode.getAttribute(o.idAttribute);
_t.iconContainer = _t.formRow.querySelector(o.formIcon);
_t.icon = _t.formRow.querySelector(o.formIconImg);
_t.visible = !_t.formRow.classList.contains(o.formRowHidden);
_t.label = _t.formRow.querySelector(o.formLabel);
_t.headerRow = _t.parentNode.getAttribute("data-header-row");
if (_t.headerRow !== null) {
_t.formHeaderRow = _t.formRow.previousElementSibling;
_t.iconContainer = _t.formHeaderRow.querySelector(o.formIcon);
_t.icon = _t.formHeaderRow.querySelector(o.formIconImg);
_t.label = _t.formHeaderRow.querySelector(o.formLabel);
} else {
_t.formHeaderRow = null;
_t.iconContainer = _t.formRow.querySelector(o.formIcon);
_t.icon = _t.formRow.querySelector(o.formIconImg);
_t.label = _t.formRow.querySelector(o.formLabel);
}
function convertToInlineSVG() {
this.removeEventListener("load", convertToInlineSVG);
@ -406,7 +415,11 @@
// Replace the current icon element with the built inline SVG
_t.iconContainer.replaceChild(newIconElement, _t.icon);
_t.icon = _t.parentNode.parentNode.querySelector(o.formIconSvg);
if (_t.headerRow !== null) {
_t.icon = _t.formHeaderRow.querySelector(o.formIconSvg);
} else {
_t.icon = _t.formRow.querySelector(o.formIconSvg);
}
};
_t.getSVGIconAndReplaceWithInline = function(srcUrl, checkCurrentColor, defaultSVG) {
@ -458,8 +471,14 @@
_t.setVisible = function(state) {
if (state) {
if (_t.headerRow === "true") {
_t.formHeaderRow.classList.remove(o.formRowHidden);
}
_t.formRow.classList.remove(o.formRowHidden);
} else {
if (_t.headerRow === "true") {
_t.formHeaderRow.classList.add(o.formRowHidden);
}
_t.formRow.classList.add(o.formRowHidden);
}
@ -555,6 +574,12 @@
} else {
this.parentNode = parentNode;
this.id = this.parentNode.getAttribute(o.idAttribute);
this.headerRow = this.parentNode.getAttribute("data-header-row");
if (this.headerRow !== null) {
this.formHeaderRow = this.parentNode.parentNode.previousElementSibling;
} else {
this.formHeaderRow = null;
}
}
var
@ -568,6 +593,27 @@
_t.url = parentNode.getAttribute("data-proxied-url");
_t.validUrl = parentNode.getAttribute("data-valid-url") === "true";
_t.ignoreRefresh = parentNode.getAttribute("data-ignore-refresh") === "true";
_t.legendButton = null;
_t.periodButton = null;
_t.upscaleButton = null;
_t.refreshButton = null;
_t.displayLegend = _t.parentNode.getAttribute("data-legend") === "true";
_t.period = null;
_t.upscale = false;
if (_t.headerRow === "true") {
_t.legendButton = _t.formHeaderRow.querySelector(o.image.legendButton);
_t.periodButton = _t.formHeaderRow.querySelector(o.image.periodButton);
_t.upscaleButton = _t.formHeaderRow.querySelector(o.image.upscaleButton);
_t.refreshButton = _t.formHeaderRow.querySelector(o.image.refreshButton);
if (_t.legendButton !== null) {
if (_t.displayLegend) {
_t.legendButton.classList.add(o.buttonActiveClass);
} else {
_t.legendButton.classList.remove(o.buttonActiveClass);
}
}
}
_t.setValuePrivate = function(value, itemState, visible) {
if (!visible) {
@ -591,9 +637,15 @@
_t.setVisible = function(state) {
if (state) {
if (_t.headerRow === "true") {
_t.formHeaderRow.classList.remove(o.formRowHidden);
}
_t.formRow.classList.remove(o.formRowHidden);
_t.activateRefresh();
} else {
if (_t.headerRow === "true") {
_t.formHeaderRow.classList.add(o.formRowHidden);
}
_t.formRow.classList.add(o.formRowHidden);
_t.deactivateRefresh();
}
@ -628,14 +680,134 @@
}, _t.updateInterval);
};
function onLegendClick() {
_t.displayLegend = !_t.displayLegend;
if (_t.displayLegend) {
_t.legendButton.classList.add(o.buttonActiveClass);
} else {
_t.legendButton.classList.remove(o.buttonActiveClass);
}
_t.url = _t.url.replace(/&legend=(true|false)/, "");
if (_t.displayLegend) {
_t.url = _t.url + "&legend=true";
} else {
_t.url = _t.url + "&legend=false";
}
_t.image.setAttribute("src", _t.url + "&t=" + Date.now());
}
function onPeriodChange(event) {
event.stopPropagation();
if (event.target.tagName.toLowerCase() !== "input") {
return;
}
_t.period = event.target.getAttribute("value");
_t.url = _t.url.replace(/&period=(h|4h|8h|12h|D|2D|3D|W|2W|M|2M|4M|Y)/, "&period=" + _t.period);
_t.image.setAttribute("src", _t.url + "&t=" + Date.now());
setTimeout(function() {
_t.modalPeriods.hide();
}, 300);
}
_t.showModalPeriods = function() {
var
content = _t.formHeaderRow.querySelector(o.image.periodRows).innerHTML;
_t.modalPeriods = new Modal(content);
_t.modalPeriods.show();
_t.modalPeriods.onHide = function() {
var
items = [].slice.call(_t.modalPeriods.container.querySelectorAll(o.formRadio));
componentHandler.downgradeElements(items);
items.forEach(function(control) {
control.removeEventListener("click", onPeriodChange);
});
_t.modalPeriods = null;
};
var
controls = [].slice.call(_t.modalPeriods.container.querySelectorAll(o.formRadio));
if (_t.period !== null) {
var
items = [].slice.call(_t.modalPeriods.container.querySelectorAll("input[type=radio]"));
items.forEach(function(radioItem) {
if (radioItem.value === _t.period) {
radioItem.checked = true;
} else {
radioItem.checked = false;
}
});
}
controls.forEach(function(control) {
componentHandler.upgradeElement(control, "MaterialRadio");
control.addEventListener("click", onPeriodChange);
});
};
function onUpscaleClick() {
_t.upscale = !_t.upscale;
if (_t.upscale) {
_t.upscaleButton.classList.add(o.buttonActiveClass);
} else {
_t.upscaleButton.classList.remove(o.buttonActiveClass);
}
if (_t.upscale) {
_t.parentNode.classList.add(o.image.upscaleClass);
} else {
_t.parentNode.classList.remove(o.image.upscaleClass);
}
}
function onRefreshClick() {
_t.image.setAttribute("src", _t.url + "&t=" + Date.now());
}
_t.destroy = function() {
var
imageParent = _t.image.parentNode;
_t.image.setAttribute("src", urlNoneIcon);
imageParent.removeChild(_t.image);
if (_t.legendButton !== null) {
componentHandler.downgradeElements([ _t.legendButton ]);
_t.legendButton.removeEventListener("click", onLegendClick);
}
if (_t.periodButton !== null) {
componentHandler.downgradeElements([ _t.periodButton ]);
_t.periodButton.removeEventListener("click", _t.showModalPeriods);
}
if (_t.upscaleButton !== null) {
componentHandler.downgradeElements([ _t.upscaleButton ]);
_t.upscaleButton.removeEventListener("click", onUpscaleClick);
}
if (_t.refreshButton !== null) {
componentHandler.downgradeElements([ _t.refreshButton ]);
_t.refreshButton.removeEventListener("click", onRefreshClick);
}
};
if (_t.legendButton !== null) {
_t.legendButton.addEventListener("click", onLegendClick);
}
if (_t.periodButton !== null) {
_t.periodButton.addEventListener("click", _t.showModalPeriods);
}
if (_t.upscaleButton !== null) {
_t.upscaleButton.addEventListener("click", onUpscaleClick);
}
if (_t.refreshButton !== null) {
_t.refreshButton.addEventListener("click", onRefreshClick);
}
if (_t.visible) {
_t.activateRefresh();
}
@ -2783,6 +2955,14 @@
colorpicker: ".colorpicker",
button: ".colorpicker__buttons > button"
},
image: {
legendButton: ".chart-legend-button",
periodButton: ".chart-period-button",
periodRows: ".mdl-form__header-rows",
upscaleButton: ".image-upscale-button",
upscaleClass: "mdl-form__image-upscale",
refreshButton: ".chart-refresh-button"
},
notify: ".mdl-notify__container",
notifyHidden: "mdl-notify--hidden",
notifyTemplateOffline: "template-offline-notify",