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)
|
||||
uint16_t addr2 = widget->getKnxAddress2();
|
||||
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 {
|
||||
if (obj_ == nullptr) return false;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
void applyStyle() override;
|
||||
void onKnxSwitch(bool value) override;
|
||||
bool evaluateConditions(float primaryValue, float secondaryValue, float tertiaryValue) override;
|
||||
|
||||
// Check if button is in checked state
|
||||
bool isChecked() const;
|
||||
@ -23,6 +25,7 @@ private:
|
||||
void applyTextAlignment();
|
||||
void createFakeShadow(lv_obj_t* parent);
|
||||
void applyFakeShadowStyle();
|
||||
void updateIcon(uint32_t codepoint);
|
||||
static int encodeUtf8(uint32_t codepoint, char* buf);
|
||||
static void clickCallback(lv_event_t* e);
|
||||
};
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
<script setup>
|
||||
import { computed, defineAsyncComponent } from 'vue';
|
||||
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';
|
||||
|
||||
// Lazy import to avoid circular dependency
|
||||
@ -105,7 +105,7 @@ const contentStyle = computed(() => {
|
||||
const iconStyle = computed(() => {
|
||||
const s = props.scale;
|
||||
const sizeIdx = props.widget.iconSize ?? props.widget.fontSize ?? 1;
|
||||
const size = fontSizes[sizeIdx] || 18;
|
||||
const size = iconFontSizes[sizeIdx] || 18;
|
||||
return {
|
||||
fontSize: `${size * s}px`,
|
||||
color: props.widget.textColor
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
<div :class="rowClass">
|
||||
<label :class="labelClass">Icon-Gr.</label>
|
||||
<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>
|
||||
</div>
|
||||
<div :class="rowClass">
|
||||
@ -104,24 +104,100 @@
|
||||
</option>
|
||||
</select>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useEditorStore } from '../../../stores/editor';
|
||||
import { sourceOptions, textSources, textSourceGroups, BUTTON_ACTIONS, fontSizes } from '../../../constants';
|
||||
import { rowClass, labelClass, inputClass, headingClass, colorInputClass, iconSelectClass, iconRemoveClass } from '../shared/styles';
|
||||
import { sourceOptions, textSources, textSourceGroups, BUTTON_ACTIONS, fontSizes, iconFontSizes } from '../../../constants';
|
||||
import { rowClass, labelClass, inputClass, headingClass, colorInputClass, iconSelectClass, iconRemoveClass, noteClass } from '../shared/styles';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
widget: { type: Object, required: true }
|
||||
});
|
||||
|
||||
defineEmits(['open-icon-picker']);
|
||||
defineEmits(['open-icon-picker', 'open-condition-icon-picker']);
|
||||
|
||||
const store = useEditorStore();
|
||||
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) {
|
||||
const allowed = new Set(options || []);
|
||||
|
||||
@ -231,7 +231,8 @@ export const WIDGET_DEFAULTS = {
|
||||
iconCodepoint: 0,
|
||||
iconPosition: 0,
|
||||
iconSize: 1,
|
||||
iconGap: 8
|
||||
iconGap: 8,
|
||||
conditions: []
|
||||
},
|
||||
led: {
|
||||
w: 60,
|
||||
|
||||
@ -402,6 +402,10 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
iconPositionY: defaults.iconPositionY || 8
|
||||
};
|
||||
|
||||
if (defaults.conditions !== undefined) {
|
||||
w.conditions = [];
|
||||
}
|
||||
|
||||
if (defaults.chart) {
|
||||
w.chart = {
|
||||
period: defaults.chart.period ?? 0,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user