Fixes
This commit is contained in:
parent
3dfd2b461d
commit
752b944b5c
@ -1033,6 +1033,13 @@ void WidgetManager::applyCachedValuesToWidgets() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (widget->getType() == WidgetType::BUTTON) {
|
||||||
|
bool state = false;
|
||||||
|
if (addr != 0 && getCachedKnxSwitch(addr, &state)) {
|
||||||
|
widget->onKnxSwitch(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Secondary address (left value)
|
// Secondary address (left value)
|
||||||
uint16_t addr2 = widget->getKnxAddress2();
|
uint16_t addr2 = widget->getKnxAddress2();
|
||||||
TextSource source2 = widget->getTextSource2();
|
TextSource source2 = widget->getTextSource2();
|
||||||
|
|||||||
@ -240,6 +240,127 @@ void ButtonWidget::applyStyle() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ButtonWidget::onKnxSwitch(bool value) {
|
||||||
|
cachedPrimaryValue_ = value ? 1.0f : 0.0f;
|
||||||
|
hasCachedPrimary_ = true;
|
||||||
|
|
||||||
|
if (obj_ && config_.isToggle) {
|
||||||
|
if (value) {
|
||||||
|
lv_obj_add_state(obj_, LV_STATE_CHECKED);
|
||||||
|
} else {
|
||||||
|
lv_obj_clear_state(obj_, LV_STATE_CHECKED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluateConditions(cachedPrimaryValue_, cachedSecondaryValue_, cachedTertiaryValue_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ButtonWidget::evaluateConditions(float primaryValue, float secondaryValue, float tertiaryValue) {
|
||||||
|
if (config_.conditionCount == 0) return false;
|
||||||
|
|
||||||
|
const StyleCondition* bestMatch = nullptr;
|
||||||
|
uint8_t bestPriority = 255;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < config_.conditionCount && i < MAX_CONDITIONS; ++i) {
|
||||||
|
const StyleCondition& cond = config_.conditions[i];
|
||||||
|
if (!cond.enabled) continue;
|
||||||
|
|
||||||
|
float checkValue = 0.0f;
|
||||||
|
bool hasValue = false;
|
||||||
|
switch (cond.source) {
|
||||||
|
case ConditionSource::PRIMARY:
|
||||||
|
checkValue = primaryValue;
|
||||||
|
hasValue = hasCachedPrimary_;
|
||||||
|
break;
|
||||||
|
case ConditionSource::SECONDARY:
|
||||||
|
checkValue = secondaryValue;
|
||||||
|
hasValue = hasCachedSecondary_;
|
||||||
|
break;
|
||||||
|
case ConditionSource::TERTIARY:
|
||||||
|
checkValue = tertiaryValue;
|
||||||
|
hasValue = hasCachedTertiary_;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasValue) continue;
|
||||||
|
|
||||||
|
bool matches = false;
|
||||||
|
switch (cond.op) {
|
||||||
|
case ConditionOp::LESS:
|
||||||
|
matches = checkValue < cond.threshold;
|
||||||
|
break;
|
||||||
|
case ConditionOp::LESS_EQUAL:
|
||||||
|
matches = checkValue <= cond.threshold;
|
||||||
|
break;
|
||||||
|
case ConditionOp::EQUAL:
|
||||||
|
matches = checkValue == cond.threshold;
|
||||||
|
break;
|
||||||
|
case ConditionOp::GREATER_EQUAL:
|
||||||
|
matches = checkValue >= cond.threshold;
|
||||||
|
break;
|
||||||
|
case ConditionOp::GREATER:
|
||||||
|
matches = checkValue > cond.threshold;
|
||||||
|
break;
|
||||||
|
case ConditionOp::NOT_EQUAL:
|
||||||
|
matches = checkValue != cond.threshold;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches && cond.priority < bestPriority) {
|
||||||
|
bestMatch = &cond;
|
||||||
|
bestPriority = cond.priority;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bestMatch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMatch->style.iconCodepoint != 0 &&
|
||||||
|
bestMatch->style.iconCodepoint != currentConditionIcon_) {
|
||||||
|
updateIcon(bestMatch->style.iconCodepoint);
|
||||||
|
currentConditionIcon_ = bestMatch->style.iconCodepoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMatch->style.flags & ConditionStyle::FLAG_USE_TEXT_COLOR) {
|
||||||
|
lv_color_t color = lv_color_make(
|
||||||
|
bestMatch->style.textColor.r,
|
||||||
|
bestMatch->style.textColor.g,
|
||||||
|
bestMatch->style.textColor.b);
|
||||||
|
if (label_) {
|
||||||
|
lv_obj_set_style_text_color(label_, color, 0);
|
||||||
|
}
|
||||||
|
if (iconLabel_) {
|
||||||
|
lv_obj_set_style_text_color(iconLabel_, color, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMatch->style.flags & ConditionStyle::FLAG_USE_BG_COLOR) {
|
||||||
|
lv_obj_set_style_bg_color(obj_, lv_color_make(
|
||||||
|
bestMatch->style.bgColor.r,
|
||||||
|
bestMatch->style.bgColor.g,
|
||||||
|
bestMatch->style.bgColor.b), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMatch->style.flags & ConditionStyle::FLAG_USE_BG_OPACITY) {
|
||||||
|
lv_obj_set_style_bg_opa(obj_, bestMatch->style.bgOpacity, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMatch->style.flags & ConditionStyle::FLAG_HIDE) {
|
||||||
|
lv_obj_add_flag(obj_, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (shadowObj_ && lv_obj_is_valid(shadowObj_)) {
|
||||||
|
lv_obj_add_flag(shadowObj_, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (shadowObj_ && lv_obj_is_valid(shadowObj_)) {
|
||||||
|
lv_obj_clear_flag(shadowObj_, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ButtonWidget::isChecked() const {
|
bool ButtonWidget::isChecked() const {
|
||||||
if (obj_ == nullptr) return false;
|
if (obj_ == nullptr) return false;
|
||||||
return (lv_obj_get_state(obj_) & LV_STATE_CHECKED) != 0;
|
return (lv_obj_get_state(obj_) & LV_STATE_CHECKED) != 0;
|
||||||
@ -288,3 +409,10 @@ void ButtonWidget::applyFakeShadowStyle() {
|
|||||||
lv_obj_set_style_radius(shadowObj_, config_.borderRadius + config_.shadow.spread, 0);
|
lv_obj_set_style_radius(shadowObj_, config_.borderRadius + config_.shadow.spread, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ButtonWidget::updateIcon(uint32_t codepoint) {
|
||||||
|
if (!iconLabel_ || codepoint == 0) return;
|
||||||
|
char iconText[5];
|
||||||
|
encodeUtf8(codepoint, iconText);
|
||||||
|
lv_label_set_text(iconLabel_, iconText);
|
||||||
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ public:
|
|||||||
|
|
||||||
lv_obj_t* create(lv_obj_t* parent) override;
|
lv_obj_t* create(lv_obj_t* parent) override;
|
||||||
void applyStyle() override;
|
void applyStyle() override;
|
||||||
|
void onKnxSwitch(bool value) override;
|
||||||
|
bool evaluateConditions(float primaryValue, float secondaryValue, float tertiaryValue) override;
|
||||||
|
|
||||||
// Check if button is in checked state
|
// Check if button is in checked state
|
||||||
bool isChecked() const;
|
bool isChecked() const;
|
||||||
@ -23,6 +25,7 @@ private:
|
|||||||
void applyTextAlignment();
|
void applyTextAlignment();
|
||||||
void createFakeShadow(lv_obj_t* parent);
|
void createFakeShadow(lv_obj_t* parent);
|
||||||
void applyFakeShadowStyle();
|
void applyFakeShadowStyle();
|
||||||
|
void updateIcon(uint32_t codepoint);
|
||||||
static int encodeUtf8(uint32_t codepoint, char* buf);
|
static int encodeUtf8(uint32_t codepoint, char* buf);
|
||||||
static void clickCallback(lv_event_t* e);
|
static void clickCallback(lv_event_t* e);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -57,7 +57,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineAsyncComponent } from 'vue';
|
import { computed, defineAsyncComponent } from 'vue';
|
||||||
import { useEditorStore } from '../../../stores/editor';
|
import { useEditorStore } from '../../../stores/editor';
|
||||||
import { fontSizes, ICON_POSITIONS } from '../../../constants';
|
import { fontSizes, iconFontSizes, ICON_POSITIONS } from '../../../constants';
|
||||||
import { getBaseStyle, justifyForAlign, getShadowStyle, getBorderStyle, clamp, hexToRgba } from '../shared/utils';
|
import { getBaseStyle, justifyForAlign, getShadowStyle, getBorderStyle, clamp, hexToRgba } from '../shared/utils';
|
||||||
|
|
||||||
// Lazy import to avoid circular dependency
|
// Lazy import to avoid circular dependency
|
||||||
@ -105,7 +105,7 @@ const contentStyle = computed(() => {
|
|||||||
const iconStyle = computed(() => {
|
const iconStyle = computed(() => {
|
||||||
const s = props.scale;
|
const s = props.scale;
|
||||||
const sizeIdx = props.widget.iconSize ?? props.widget.fontSize ?? 1;
|
const sizeIdx = props.widget.iconSize ?? props.widget.fontSize ?? 1;
|
||||||
const size = fontSizes[sizeIdx] || 18;
|
const size = iconFontSizes[sizeIdx] || 18;
|
||||||
return {
|
return {
|
||||||
fontSize: `${size * s}px`,
|
fontSize: `${size * s}px`,
|
||||||
color: props.widget.textColor
|
color: props.widget.textColor
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
<div :class="rowClass">
|
<div :class="rowClass">
|
||||||
<label :class="labelClass">Icon-Gr.</label>
|
<label :class="labelClass">Icon-Gr.</label>
|
||||||
<select :class="inputClass" v-model.number="widget.iconSize">
|
<select :class="inputClass" v-model.number="widget.iconSize">
|
||||||
<option v-for="(size, idx) in fontSizes" :key="idx" :value="idx">{{ size }}</option>
|
<option v-for="(size, idx) in iconFontSizes" :key="idx" :value="idx">{{ size }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div :class="rowClass">
|
<div :class="rowClass">
|
||||||
@ -104,24 +104,100 @@
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div :class="rowClass"><label :class="labelClass">KNX Status</label>
|
||||||
|
<select :class="inputClass" v-model.number="widget.knxAddr">
|
||||||
|
<option :value="0">-- Waehlen --</option>
|
||||||
|
<option v-for="addr in readableAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||||
|
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div :class="noteClass">Tipp: Status-GA sollte DPT 1.x sein. Bedingungen z. B. <code>eq 1</code> = EIN, <code>eq 0</code> = AUS.</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Conditions -->
|
||||||
|
<h4 :class="headingClass">Bedingungen</h4>
|
||||||
|
<div :class="rowClass">
|
||||||
|
<label :class="labelClass">Anzahl</label>
|
||||||
|
<select :class="inputClass" v-model.number="conditionCount">
|
||||||
|
<option :value="0">Keine</option>
|
||||||
|
<option :value="1">1</option>
|
||||||
|
<option :value="2">2</option>
|
||||||
|
<option :value="3">3</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div v-for="(cond, idx) in conditions" :key="idx" class="border border-border rounded-lg px-2.5 py-2 mb-2 bg-panel-2">
|
||||||
|
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
|
||||||
|
<label class="w-[50px]">Wenn</label>
|
||||||
|
<select class="w-[60px] bg-white border border-border rounded-md px-2 py-1 text-[11px]" v-model="cond.op">
|
||||||
|
<option value="lt"><</option>
|
||||||
|
<option value="lte"><=</option>
|
||||||
|
<option value="eq">=</option>
|
||||||
|
<option value="gte">>=</option>
|
||||||
|
<option value="gt">></option>
|
||||||
|
<option value="neq">!=</option>
|
||||||
|
</select>
|
||||||
|
<input class="flex-1 bg-white border border-border rounded-md px-2 py-1 text-[11px]" type="number" step="0.1" v-model.number="cond.threshold" placeholder="Schwelle">
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
|
||||||
|
<label class="w-[50px]">Icon</label>
|
||||||
|
<button class="flex-1 bg-white border border-border rounded-md px-2.5 py-1 text-[11px] flex items-center justify-center gap-1 cursor-pointer hover:bg-[#e4ebf2]" @click="$emit('open-condition-icon-picker', idx)">
|
||||||
|
<span v-if="cond.icon" class="material-symbols-outlined text-[16px]">{{ String.fromCodePoint(cond.icon) }}</span>
|
||||||
|
<span v-else>Kein Icon</span>
|
||||||
|
</button>
|
||||||
|
<button v-if="cond.icon" class="w-6 h-6 rounded-md border border-red-200 bg-[#f7dede] text-[#b3261e] grid place-items-center text-[11px] cursor-pointer hover:bg-[#f2cfcf]" @click="cond.icon = 0">x</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-[11px] text-muted">
|
||||||
|
<label class="w-[50px]">Farbe</label>
|
||||||
|
<input class="h-[22px] w-[32px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="cond.textColor">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useEditorStore } from '../../../stores/editor';
|
import { useEditorStore } from '../../../stores/editor';
|
||||||
import { sourceOptions, textSources, textSourceGroups, BUTTON_ACTIONS, fontSizes } from '../../../constants';
|
import { sourceOptions, textSources, textSourceGroups, BUTTON_ACTIONS, fontSizes, iconFontSizes } from '../../../constants';
|
||||||
import { rowClass, labelClass, inputClass, headingClass, colorInputClass, iconSelectClass, iconRemoveClass } from '../shared/styles';
|
import { rowClass, labelClass, inputClass, headingClass, colorInputClass, iconSelectClass, iconRemoveClass, noteClass } from '../shared/styles';
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
widget: { type: Object, required: true }
|
widget: { type: Object, required: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(['open-icon-picker']);
|
defineEmits(['open-icon-picker', 'open-condition-icon-picker']);
|
||||||
|
|
||||||
const store = useEditorStore();
|
const store = useEditorStore();
|
||||||
const writeableAddresses = computed(() => store.knxAddresses.filter(a => a.write));
|
const writeableAddresses = computed(() => store.knxAddresses.filter(a => a.write));
|
||||||
|
const readableAddresses = computed(() => store.knxAddresses);
|
||||||
|
|
||||||
|
const conditions = computed(() => props.widget?.conditions ?? []);
|
||||||
|
|
||||||
|
const conditionCount = computed({
|
||||||
|
get() {
|
||||||
|
return conditions.value.length || 0;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (!props.widget) return;
|
||||||
|
const target = Math.max(0, Math.min(value, 3));
|
||||||
|
if (!Array.isArray(props.widget.conditions)) {
|
||||||
|
props.widget.conditions = [];
|
||||||
|
}
|
||||||
|
while (props.widget.conditions.length < target) {
|
||||||
|
props.widget.conditions.push({
|
||||||
|
source: 'primary',
|
||||||
|
threshold: 1,
|
||||||
|
op: 'eq',
|
||||||
|
priority: props.widget.conditions.length,
|
||||||
|
icon: 0,
|
||||||
|
textColor: '#FFFFFF'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (props.widget.conditions.length > target) {
|
||||||
|
props.widget.conditions = props.widget.conditions.slice(0, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function groupedSources(options) {
|
function groupedSources(options) {
|
||||||
const allowed = new Set(options || []);
|
const allowed = new Set(options || []);
|
||||||
|
|||||||
@ -231,7 +231,8 @@ export const WIDGET_DEFAULTS = {
|
|||||||
iconCodepoint: 0,
|
iconCodepoint: 0,
|
||||||
iconPosition: 0,
|
iconPosition: 0,
|
||||||
iconSize: 1,
|
iconSize: 1,
|
||||||
iconGap: 8
|
iconGap: 8,
|
||||||
|
conditions: []
|
||||||
},
|
},
|
||||||
led: {
|
led: {
|
||||||
w: 60,
|
w: 60,
|
||||||
|
|||||||
@ -402,6 +402,10 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
iconPositionY: defaults.iconPositionY || 8
|
iconPositionY: defaults.iconPositionY || 8
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (defaults.conditions !== undefined) {
|
||||||
|
w.conditions = [];
|
||||||
|
}
|
||||||
|
|
||||||
if (defaults.chart) {
|
if (defaults.chart) {
|
||||||
w.chart = {
|
w.chart = {
|
||||||
period: defaults.chart.period ?? 0,
|
period: defaults.chart.period ?? 0,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user