Skip to content

Commit

Permalink
Sitemap editor: Add Buttongrid Button support (#2755)
Browse files Browse the repository at this point in the history
Refs openhab/openhab-core#4377.

The most recent addition to the sitemap syntax allows defining buttons
nested as components inside the Buttongrid component.

---------

Signed-off-by: Mark Herwege <[email protected]>
  • Loading branch information
mherwege authored Oct 4, 2024
1 parent 2975d78 commit 6c46b79
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 117 deletions.
32 changes: 16 additions & 16 deletions bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@
item: 'item=',
staticIcon: 'staticIcon=',
icon: 'icon=',
widgetattr: ['url=', 'refresh=', 'service=', 'height=', 'minValue=', 'maxValue=', 'step=', 'encoding=', 'yAxisDecimalPattern=', 'inputHint=', 'columns='],
widgetattr: ['url=', 'refresh=', 'service=', 'height=', 'minValue=', 'maxValue=', 'step=', 'encoding=', 'yAxisDecimalPattern=', 'inputHint=', 'columns=', 'row=', 'column='],
widgetboolattr: ['legend='],
widgetfrcitmattr: 'forceasitem=',
widgetmapattr: 'mappings=',
widgetbuttonattr: 'buttons=',
widgetbuttonsattr:'buttons=',
widgetvisiattr: 'visibility=',
widgetcolorattr: ['labelcolor=', 'valuecolor=', 'iconcolor='],
widgetswitchattr: 'switchSupport',
widgetronlyattr: 'releaseOnly',
widgetstatelattr: 'stateless',
widgetclickattr: 'click=',
widgetreleaseattr:'release=',
widgetperiodattr: 'period=',
nlwidget: ['Switch ', 'Selection ', 'Slider ', 'Setpoint ', 'Input ', 'Video ', 'Chart ', 'Webview ', 'Colorpicker ', 'Mapview ', 'Buttongrid ', 'Default '],
lwidget: ['Text ', 'Group ', 'Image ', 'Frame '],
nlwidget: ['Switch ', 'Selection ', 'Slider ', 'Setpoint ', 'Input ', 'Video ', 'Chart ', 'Webview ', 'Colorpicker ', 'Mapview ', 'Button ', 'Default '],
lwidget: ['Text ', 'Group ', 'Image ', 'Frame ', 'Buttongrid '],
lparen: '(',
rparen: ')',
lbrace: '{',
Expand All @@ -46,7 +49,7 @@
number: /[+-]?[0-9]+(?:\.[0-9]*)?/,
string: { match: /"(?:\\["\\]|[^\n"\\])*"/, value: x => x.slice(1, -1) }
})
const requiresItem = ['Group', 'Chart', 'Switch', 'Mapview', 'Slider', 'Selection', 'Setpoint', 'Input ', 'Colorpicker', 'Default']
const requiresItem = ['Group', 'Chart', 'Switch', 'Mapview', 'Slider', 'Selection', 'Setpoint', 'Input ', 'Colorpicker', 'Button', 'Default']

function getSitemap(d) {
return {
Expand Down Expand Up @@ -75,20 +78,14 @@
}
}

// if icon exists remove staticIcon, if not set icon to staticIcon and make saticIcon=true
// if icon exists remove staticIcon, if not set icon to staticIcon and make staticIcon=true
if (widget.config.icon) {
delete widget.config.staticIcon
}
if (widget.config.staticIcon) {
widget.config.icon = widget.config.staticIcon
widget.config.staticIcon = true
}

// reject widgets with missing parameters
if (requiresItem.includes(widget.component) && !widget.config.item) return reject
if ((widget.component === 'Video' || widget.component === 'Webview') && !widget.config.url) return reject
if (widget.component === 'Chart' && !widget.config.period) return reject

return widget
}
%}
Expand All @@ -113,9 +110,12 @@ WidgetAttrs -> WidgetAttr
| WidgetAttrs _ WidgetAttr {% (d) => d[0].concat([d[2]]) %}
WidgetAttr -> %widgetswitchattr {% (d) => ['switchEnabled', true] %}
| %widgetronlyattr {% (d) => ['releaseOnly', true] %}
| %widgetstatelattr {% (d) => ['stateless', true] %}
| %widgetfrcitmattr _ WidgetBooleanAttrValue {% (d) => ['forceAsItem', d[2]] %}
| %widgetboolattr _ WidgetBooleanAttrValue {% (d) => [d[0].value, d[2]] %}
| %widgetperiodattr _ WidgetPeriodAttrValue {% (d) => ['period', d[2]] %}
| %widgetclickattr _ WidgetAttrValue {% (d) => ['cmd', d[2]] %}
| %widgetreleaseattr _ WidgetAttrValue {% (d) => ['releaseCmd', d[2]] %}
| %icon _ WidgetIconRulesAttrValue {% (d) => ['iconrules', d[2]] %}
| %icon _ WidgetIconAttrValue {% (d) => [d[0].value, d[2].join("")] %}
| %staticIcon _ WidgetIconAttrValue {% (d) => [d[0].value, d[2].join("")] %}
Expand Down Expand Up @@ -143,7 +143,7 @@ WidgetAttrValue -> %number
| %string {% (d) => d[0].value %}
WidgetMappingsAttrName -> %widgetmapattr
WidgetMappingsAttrValue -> %lbracket _ Mappings _ %rbracket {% (d) => d[2] %}
WidgetButtonsAttrName -> %widgetbuttonattr
WidgetButtonsAttrName -> %widgetbuttonsattr
WidgetButtonsAttrValue -> %lbracket _ Buttons _ %rbracket {% (d) => d[2] %}
WidgetVisibilityAttrName -> %widgetvisiattr
WidgetVisibilityAttrValue -> %lbracket _ Visibilities _ %rbracket {% (d) => d[2] %}
Expand All @@ -157,9 +157,9 @@ Mapping -> Command _ %colon _ Command _ %equals _ Label
| Command _ %colon _ Command _ %equals _ Label _ %equals _ WidgetIconAttrValue {% (d) => d[0] + ':' + d[4] + '=' + d[8] + '=' + d[12].join("") %}
| Command _ %equals _ Label _ %equals _ WidgetIconAttrValue {% (d) => d[0] + '=' + d[4] + '=' + d[8].join("") %}

Buttons -> Button {% (d) => [d[0]] %}
| Buttons _ %comma _ Button {% (d) => d[0].concat([d[4]]) %}
Button -> %number _ %colon _ %number _ %colon _ ButtonValue {% (d) => { return { 'row': parseInt(d[0].value), 'column': parseInt(d[4].value), 'command': d[8] } } %}
Buttons -> ButtonDef {% (d) => [d[0]] %}
| Buttons _ %comma _ ButtonDef {% (d) => d[0].concat([d[4]]) %}
ButtonDef -> %number _ %colon _ %number _ %colon _ ButtonValue {% (d) => { return { 'row': parseInt(d[0].value), 'column': parseInt(d[4].value), 'command': d[8] } } %}
ButtonValue -> Command _ %equals _ Label {% (d) => d[0] + '=' + d[4] %}
| Command _ %equals _ Label _ %equals _ WidgetIconAttrValue {% (d) => d[0] + '=' + d[4] + '=' + d[8].join("") %}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ describe('dslUtil', () => {

it('renders a single simple widget correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Switch', {
item: 'TestItem',
label: 'Test Switch'
Expand All @@ -52,8 +50,6 @@ describe('dslUtil', () => {

it('renders a widget with icon correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Switch', {
item: 'TestItem',
label: 'Test Switch',
Expand All @@ -70,8 +66,6 @@ describe('dslUtil', () => {

it('renders a widget with static icon correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Switch', {
item: 'TestItem',
label: 'Test Switch',
Expand Down Expand Up @@ -106,8 +100,6 @@ describe('dslUtil', () => {

it('renders a widget with mappings correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Selection', {
item: 'Scene_General',
mappings: [
Expand All @@ -125,8 +117,6 @@ describe('dslUtil', () => {

it('renders a Buttongrid widget correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Buttongrid', {
item: 'Scene_General',
buttons: [
Expand All @@ -142,10 +132,37 @@ describe('dslUtil', () => {
expect(sitemap[1]).toEqual(' Buttongrid item=Scene_General buttons=[1:1:1=Morning, 1:2:2=Evening, 1:3:10="Cinéma", 2:1:11=TV, 2:2:3="Bed time", 2:3:4=Night=moon]')
})

it('renders a Buttongrid with Buttons widget correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = addWidget(component, 'Buttongrid', {
label: 'Scenes',
staticIcon: true,
icon: 'screen'
})
addWidget(widget, 'Button', {
row: 1,
column: 1,
item: 'Scene_General',
label: 'Morning',
stateless: true,
cmd: 1
})
addWidget(widget, 'Button', {
row: 1,
column: 2,
item: 'Scene_General',
label: 'Cinéma',
cmd: '10',
releaseCmd: 'test 11'
})
const sitemap = dslUtil.toDsl(component).split('\n')
expect(sitemap[1]).toEqual(' Buttongrid label="Scenes" staticIcon=screen {')
expect(sitemap[2]).toEqual(' Button row=1 column=1 item=Scene_General label="Morning" stateless click=1')
expect(sitemap[3]).toEqual(' Button row=1 column=2 item=Scene_General label="Cinéma" click="10" release="test 11"')
})

it('renders a widget with mappings and string keys correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Selection', {
item: 'Echos',
mappings: [
Expand All @@ -160,8 +177,6 @@ describe('dslUtil', () => {

it('renders a widget with mappings and release command correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Switch', {
item: 'pressAndRelease',
mappings: ['ON:OFF=ON']
Expand All @@ -172,8 +187,6 @@ describe('dslUtil', () => {

it('renders a widget with mappings and release command and string commands correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Switch', {
item: 'pressAndRelease',
mappings: ['"ON command":"OFF command"="ON"']
Expand All @@ -184,8 +197,6 @@ describe('dslUtil', () => {

it('renders a widget with 0 value parameter correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Slider', {
item: 'Dimmer1',
minValue: 0,
Expand All @@ -198,8 +209,6 @@ describe('dslUtil', () => {

it('renders widget with visibility correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Text', {
item: 'Test',
visibility: [
Expand All @@ -214,8 +223,6 @@ describe('dslUtil', () => {

it('renders widget with visibility and text condition correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Switch', {
item: 'Test',
visibility: [
Expand All @@ -229,8 +236,6 @@ describe('dslUtil', () => {

it('renders widget with valuecolor correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Text', {
item: 'Temperature',
valuecolor: [
Expand All @@ -247,8 +252,6 @@ describe('dslUtil', () => {

it('renders widget with valuecolor and text condition correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Text', {
item: 'Temperature',
valuecolor: [
Expand All @@ -261,8 +264,6 @@ describe('dslUtil', () => {

it('renders widget with valuecolor and AND condition correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Text', {
item: 'Temperature',
valuecolor: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,67 @@ describe('SitemapCode', () => {
})
})

it('parses a Buttongrid with Button components correctly', async () => {
expect(wrapper.vm.sitemapDsl).toBeDefined()
// simulate updating the sitemap in code
const sitemap = [
'sitemap test label="Test" {',
' Buttongrid label="Scenes" staticIcon=screen {',
' Button row=1 column=1 item=Scene_General label=Morning stateless click=1',
' Button row=1 column=2 item=Scene_General label="Cinéma" click="10" release="11"',
' }',
'}',
''
].join('\n')
wrapper.vm.updateSitemap(sitemap)
expect(wrapper.vm.sitemapDsl).toMatch(/^sitemap test label="Test"/)
expect(wrapper.vm.parsedSitemap.error).toBeFalsy()

await wrapper.vm.$nextTick()

// check whether an 'updated' event was emitted and its payload
// (should contain the parsing result for the new sitemap definition)
const events = wrapper.emitted().updated
expect(events).toBeTruthy()
expect(events.length).toBe(1)
const payload = events[0][0]
expect(payload.slots).toBeDefined()
expect(payload.slots.widgets).toBeDefined()
expect(payload.slots.widgets.length).toBe(1)
expect(payload.slots.widgets[0]).toEqual({
component: 'Buttongrid',
config: {
label: 'Scenes',
staticIcon: true,
icon: 'screen'
},
slots: {
widgets: [{
component: 'Button',
config: {
row: 1,
column: 1,
item: 'Scene_General',
label: 'Morning',
stateless: true,
cmd: 1
}
},
{
component: 'Button',
config: {
row: 1,
column: 2,
item: 'Scene_General',
label: 'Cinéma',
cmd: '10',
releaseCmd: '11'
}
}]
}
})
})

it('parses a mapping code back to a mapping on a component', async () => {
expect(wrapper.vm.sitemapDsl).toBeDefined()
// simulate updating the sitemap in code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ function writeWidget (widget, indent) {
dsl += ' releaseOnly'
} else if (key === 'forceAsItem') {
dsl += ' forceasitem=' + widget.config[key]
} else if (key === 'stateless') {
dsl += ' stateless'
} else if (key === 'icon') {
if (widget.config.staticIcon) {
dsl += ' staticIcon=' + widget.config[key]
Expand All @@ -20,6 +22,10 @@ function writeWidget (widget, indent) {
} else if (key !== 'staticIcon') {
if (key === 'iconrules') {
dsl += ' icon='
} else if (key === 'cmd') {
dsl += ' click='
} else if (key === 'releaseCmd') {
dsl += ' release='
} else {
dsl += ` ${key}=`
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<f7-treeview-item selectable :label="widget.config.label"
<f7-treeview-item selectable :label="widget.config.label ? widget.config.label : ((widget.component === 'Button') ? widget.config.cmd : '')"
:icon-ios="icon('ios')" :icon-aurora="icon('aurora')" :icon-md="icon('md')"
:textColor="iconColor" :color="'blue'"
:selected="selected && selected === widget"
Expand Down Expand Up @@ -44,6 +44,8 @@ export default {
return 'f7:map'
case 'Buttongrid':
return 'f7:square_grid_3x2'
case 'Button':
return 'f7:square_fill_line_vertical_square'
case 'Default':
return 'f7:rectangle'
case 'Text':
Expand All @@ -59,7 +61,8 @@ export default {
}
},
subtitle () {
return this.widget.component + ((this.widget.config && this.widget.config.item) ? ': ' + this.widget.config.item : '')
const buttonPosition = this.widget.component === 'Button' ? ' (' + (this.widget.config?.row ?? '-') + ',' + (this.widget.config?.column ?? '-') + ')' : ''
return this.widget.component + ((this.widget.config && this.widget.config.item) ? ': ' + this.widget.config.item : '') + buttonPosition
},
select (event) {
let self = this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@
placeholder="##0.0"
validate pattern="^(?:'[0#.,;E]?'|[^0#.,;E'])*((#[,#]*|0)[,0]*)(\.(0+#*|#+))?(?:E0+)?(?:';'|[^;])*(?:;(?:'[0#.,;E]?'|[^0#.,;E'])*((#[,#]*|0)[,0]*)(\.(0+#*|#+))?(?:E0+)?.*)?$"
:value="widget.config.yAxisDecimalPattern" @input="updateParameter('yAxisDecimalPattern', $event)" clear-button />
<f7-list-input v-if="supports('row')" label="Row" type="number" required validate min="1" :value="widget.config.row" @input="updateParameter('row', $event)" clear-button />
<f7-list-input v-if="supports('column')" label="Column" type="number" required validate min="1" max="12" :value="widget.config.column" @input="updateParameter('column', $event)" clear-button />
<f7-list-input v-if="supports('cmd')" label="Click command" type="text" required validate :value="widget.config.cmd" @input="updateParameter('cmd', $event)" clear-button />
<f7-list-input v-if="supports('releaseCmd')" label="Release command" type="text" :value="widget.config.releaseCmd" @input="updateParameter('releaseCmd', $event)" clear-button />
<f7-list-item v-if="supports('stateless')" title="Stateless">
<f7-toggle slot="after" :checked="widget.config.stateless" @toggle:change="widget.config.stateless = $event" />
</f7-list-item>
<f7-list-item v-if="supports('switchEnabled')" title="Switch enabled">
<f7-toggle slot="after" :checked="widget.config.switchEnabled" @toggle:change="widget.config.switchEnabled = $event" />
</f7-list-item>
Expand Down Expand Up @@ -113,6 +120,7 @@ export default {
Slider: ['switchEnabled', 'releaseOnly', 'minValue', 'maxValue', 'step'],
Setpoint: ['minValue', 'maxValue', 'step'],
Input: ['inputHint'],
Button: ['row', 'column', 'stateless', 'cmd', 'releaseCmd'],
Default: ['height']
}
this.ENCODING_DEFS = [
Expand Down
Loading

0 comments on commit 6c46b79

Please sign in to comment.