Fixes
This commit is contained in:
parent
adf0f26da0
commit
3dfd2b461d
@ -434,7 +434,7 @@ WidgetConfig WidgetConfig::createButton(uint8_t id, int16_t x, int16_t y,
|
||||
strncpy(cfg.text, labelText, MAX_TEXT_LEN - 1);
|
||||
cfg.fontSize = 1;
|
||||
cfg.textAlign = static_cast<uint8_t>(TextAlign::CENTER);
|
||||
cfg.isContainer = true;
|
||||
cfg.isContainer = false;
|
||||
cfg.textColor = {255, 255, 255};
|
||||
cfg.bgColor = {33, 150, 243}; // Blue
|
||||
cfg.bgOpacity = 255;
|
||||
|
||||
@ -89,58 +89,73 @@ static void latin1_to_utf8(const char* src, size_t src_len, char* dst, size_t ds
|
||||
dst[di] = '\0';
|
||||
}
|
||||
|
||||
static WidgetConfig makeButtonLabelChild(const WidgetConfig& button) {
|
||||
WidgetConfig label = WidgetConfig::createLabel(0, 0, 0, button.text);
|
||||
label.parentId = button.id;
|
||||
if (button.width > 0) label.width = button.width;
|
||||
if (button.height > 0) label.height = button.height;
|
||||
label.fontSize = button.fontSize;
|
||||
label.textAlign = button.textAlign;
|
||||
label.textColor = button.textColor;
|
||||
label.textSource = TextSource::STATIC;
|
||||
label.bgOpacity = 0;
|
||||
label.borderRadius = 0;
|
||||
label.shadow.enabled = false;
|
||||
// Preserve existing icon config if any
|
||||
label.iconCodepoint = button.iconCodepoint;
|
||||
label.iconPosition = button.iconPosition;
|
||||
label.iconSize = button.iconSize;
|
||||
label.iconGap = button.iconGap;
|
||||
if (label.text[0] == '\0') {
|
||||
strncpy(label.text, "Button", MAX_TEXT_LEN - 1);
|
||||
label.text[MAX_TEXT_LEN - 1] = '\0';
|
||||
}
|
||||
return label;
|
||||
static bool color_equals(const Color& a, const Color& b) {
|
||||
return a.r == b.r && a.g == b.g && a.b == b.b;
|
||||
}
|
||||
|
||||
static void ensureButtonLabels(ScreenConfig& screen) {
|
||||
bool hasLabelChild[MAX_WIDGETS] = {};
|
||||
static bool is_auto_button_label(const WidgetConfig& button, const WidgetConfig& label) {
|
||||
if (label.type != WidgetType::LABEL) return false;
|
||||
if (label.parentId < 0 || label.parentId != static_cast<int8_t>(button.id)) return false;
|
||||
if (label.textSource != TextSource::STATIC) return false;
|
||||
bool textMatches = (strncmp(label.text, button.text, MAX_TEXT_LEN) == 0);
|
||||
if (!textMatches && button.text[0] == '\0') {
|
||||
if (strncmp(label.text, "Button", MAX_TEXT_LEN) == 0 || label.text[0] == '\0') {
|
||||
textMatches = true;
|
||||
}
|
||||
}
|
||||
if (!textMatches) return false;
|
||||
if (label.fontSize != button.fontSize) return false;
|
||||
if (label.textAlign != button.textAlign) return false;
|
||||
if (!color_equals(label.textColor, button.textColor)) return false;
|
||||
if (label.bgOpacity != 0) return false;
|
||||
if (label.borderRadius != 0) return false;
|
||||
if (label.shadow.enabled) return false;
|
||||
if (label.iconCodepoint != button.iconCodepoint) return false;
|
||||
if (label.iconPosition != button.iconPosition) return false;
|
||||
if (label.iconSize != button.iconSize) return false;
|
||||
if (label.iconGap != button.iconGap) return false;
|
||||
if (label.x != 0 || label.y != 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void normalizeButtons(ScreenConfig& screen) {
|
||||
uint8_t removeIds[MAX_WIDGETS] = {};
|
||||
uint8_t removeCount = 0;
|
||||
|
||||
// Remove legacy auto-generated label children for buttons
|
||||
for (uint8_t i = 0; i < screen.widgetCount; i++) {
|
||||
const WidgetConfig& w = screen.widgets[i];
|
||||
if (w.type == WidgetType::LABEL && w.parentId >= 0 && w.parentId < MAX_WIDGETS) {
|
||||
hasLabelChild[w.parentId] = true;
|
||||
if (w.type != WidgetType::LABEL || w.parentId < 0) continue;
|
||||
|
||||
WidgetConfig* parent = screen.findWidget(static_cast<uint8_t>(w.parentId));
|
||||
if (!parent || parent->type != WidgetType::BUTTON) continue;
|
||||
|
||||
bool forceRemove = parent->text[0] != '\0';
|
||||
if (forceRemove || is_auto_button_label(*parent, w)) {
|
||||
if (removeCount < MAX_WIDGETS) {
|
||||
removeIds[removeCount++] = w.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t initialCount = screen.widgetCount;
|
||||
for (uint8_t i = 0; i < initialCount; i++) {
|
||||
for (uint8_t i = 0; i < removeCount; i++) {
|
||||
screen.removeWidget(removeIds[i]);
|
||||
}
|
||||
|
||||
// Update container flag based on remaining children
|
||||
for (uint8_t i = 0; i < screen.widgetCount; i++) {
|
||||
WidgetConfig& w = screen.widgets[i];
|
||||
if (w.type != WidgetType::BUTTON) continue;
|
||||
|
||||
w.isContainer = true;
|
||||
|
||||
if (w.id < MAX_WIDGETS && hasLabelChild[w.id]) continue;
|
||||
|
||||
WidgetConfig label = makeButtonLabelChild(w);
|
||||
int newId = screen.addWidget(label);
|
||||
if (newId < 0) {
|
||||
ESP_LOGW(TAG, "No space to add label child for button %d", w.id);
|
||||
w.isContainer = false;
|
||||
continue;
|
||||
bool hasChild = false;
|
||||
for (uint8_t j = 0; j < screen.widgetCount; j++) {
|
||||
const WidgetConfig& child = screen.widgets[j];
|
||||
if (child.parentId >= 0 && child.parentId == static_cast<int8_t>(w.id)) {
|
||||
hasChild = true;
|
||||
break;
|
||||
}
|
||||
if (w.id < MAX_WIDGETS) {
|
||||
hasLabelChild[w.id] = true;
|
||||
}
|
||||
w.isContainer = hasChild;
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,7 +203,7 @@ void WidgetManager::createDefaultConfig() {
|
||||
progBtn.bgColor = {200, 50, 50}; // Red
|
||||
screen.addWidget(progBtn);
|
||||
|
||||
ensureButtonLabels(screen);
|
||||
normalizeButtons(screen);
|
||||
|
||||
config_->startScreenId = screen.id;
|
||||
config_->standbyEnabled = false;
|
||||
@ -352,7 +367,7 @@ void WidgetManager::applyScreenLocked(uint8_t screenId) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureButtonLabels(*screen);
|
||||
normalizeButtons(*screen);
|
||||
|
||||
if (modalContainer_) {
|
||||
ESP_LOGI(TAG, "Closing modal first");
|
||||
@ -2286,7 +2301,7 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
|
||||
if (!parseWidgets(widgets, screen)) {
|
||||
screen.widgetCount = 0;
|
||||
}
|
||||
ensureButtonLabels(screen);
|
||||
normalizeButtons(screen);
|
||||
|
||||
newConfig->screenCount++;
|
||||
}
|
||||
@ -2305,7 +2320,7 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
|
||||
cJSON_Delete(root);
|
||||
return false;
|
||||
}
|
||||
ensureButtonLabels(screen);
|
||||
normalizeButtons(screen);
|
||||
}
|
||||
|
||||
cJSON* startScreen = cJSON_GetObjectItem(root, "startScreen");
|
||||
|
||||
@ -12,6 +12,12 @@ static lv_text_align_t toLvTextAlign(uint8_t align) {
|
||||
return LV_TEXT_ALIGN_CENTER;
|
||||
}
|
||||
|
||||
static lv_align_t toLvAlign(uint8_t align) {
|
||||
if (align == static_cast<uint8_t>(TextAlign::LEFT)) return LV_ALIGN_LEFT_MID;
|
||||
if (align == static_cast<uint8_t>(TextAlign::RIGHT)) return LV_ALIGN_RIGHT_MID;
|
||||
return LV_ALIGN_CENTER;
|
||||
}
|
||||
|
||||
static lv_flex_align_t toFlexAlign(uint8_t align) {
|
||||
if (align == static_cast<uint8_t>(TextAlign::LEFT)) return LV_FLEX_ALIGN_START;
|
||||
if (align == static_cast<uint8_t>(TextAlign::RIGHT)) return LV_FLEX_ALIGN_END;
|
||||
@ -130,7 +136,7 @@ void ButtonWidget::setupFlexLayout() {
|
||||
void ButtonWidget::applyTextAlignment() {
|
||||
if (label_ == nullptr) return;
|
||||
lv_obj_set_style_text_align(label_, toLvTextAlign(config_.textAlign), 0);
|
||||
lv_obj_center(label_);
|
||||
lv_obj_align(label_, toLvAlign(config_.textAlign), 0, 0);
|
||||
}
|
||||
|
||||
lv_obj_t* ButtonWidget::create(lv_obj_t* parent) {
|
||||
|
||||
@ -13,8 +13,56 @@
|
||||
<label :class="labelClass">Text</label><input :class="inputClass" type="text" v-model="widget.text">
|
||||
</div>
|
||||
|
||||
<!-- Typography -->
|
||||
<h4 :class="headingClass">Typo</h4>
|
||||
<div :class="rowClass"><label :class="labelClass">Schriftgr.</label>
|
||||
<select :class="inputClass" v-model.number="widget.fontSize">
|
||||
<option v-for="(size, idx) in fontSizes" :key="idx" :value="idx">{{ size }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div :class="rowClass"><label :class="labelClass">Ausrichtung</label>
|
||||
<select :class="inputClass" v-model.number="widget.textAlign">
|
||||
<option :value="0">Links</option>
|
||||
<option :value="1">Zentriert</option>
|
||||
<option :value="2">Rechts</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Icon -->
|
||||
<h4 :class="headingClass">Icon</h4>
|
||||
<div :class="rowClass">
|
||||
<label :class="labelClass">Icon</label>
|
||||
<button :class="iconSelectClass" @click="$emit('open-icon-picker')">
|
||||
<span v-if="widget.iconCodepoint" class="material-symbols-outlined text-[20px]">{{ String.fromCodePoint(widget.iconCodepoint) }}</span>
|
||||
<span v-else>Kein Icon</span>
|
||||
</button>
|
||||
<button v-if="widget.iconCodepoint" :class="iconRemoveClass" @click="widget.iconCodepoint = 0">x</button>
|
||||
</div>
|
||||
<template v-if="widget.iconCodepoint">
|
||||
<div :class="rowClass">
|
||||
<label :class="labelClass">Position</label>
|
||||
<select :class="inputClass" v-model.number="widget.iconPosition">
|
||||
<option :value="0">Links</option>
|
||||
<option :value="1">Rechts</option>
|
||||
<option :value="2">Oben</option>
|
||||
<option :value="3">Unten</option>
|
||||
</select>
|
||||
</div>
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
<div :class="rowClass">
|
||||
<label :class="labelClass">Abstand</label>
|
||||
<input :class="inputClass" type="number" v-model.number="widget.iconGap" min="0" max="50">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Style -->
|
||||
<h4 :class="headingClass">Stil</h4>
|
||||
<div :class="rowClass"><label :class="labelClass">Textfarbe</label><input :class="colorInputClass" type="color" v-model="widget.textColor"></div>
|
||||
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input :class="colorInputClass" type="color" v-model="widget.bgColor"></div>
|
||||
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="widget.bgOpacity"></div>
|
||||
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="widget.radius"></div>
|
||||
@ -63,13 +111,15 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useEditorStore } from '../../../stores/editor';
|
||||
import { sourceOptions, textSources, textSourceGroups, BUTTON_ACTIONS } from '../../../constants';
|
||||
import { rowClass, labelClass, inputClass, headingClass, colorInputClass } from '../shared/styles';
|
||||
import { sourceOptions, textSources, textSourceGroups, BUTTON_ACTIONS, fontSizes } from '../../../constants';
|
||||
import { rowClass, labelClass, inputClass, headingClass, colorInputClass, iconSelectClass, iconRemoveClass } from '../shared/styles';
|
||||
|
||||
defineProps({
|
||||
widget: { type: Object, required: true }
|
||||
});
|
||||
|
||||
defineEmits(['open-icon-picker']);
|
||||
|
||||
const store = useEditorStore();
|
||||
const writeableAddresses = computed(() => store.knxAddresses.filter(a => a.write));
|
||||
|
||||
|
||||
@ -217,7 +217,7 @@ export const WIDGET_DEFAULTS = {
|
||||
textSrc: 0,
|
||||
fontSize: 1,
|
||||
textAlign: TEXT_ALIGNS.CENTER,
|
||||
isContainer: true,
|
||||
isContainer: false,
|
||||
textColor: '#FFFFFF',
|
||||
bgColor: '#2E7DD1',
|
||||
bgOpacity: 255,
|
||||
|
||||
@ -124,60 +124,30 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
return activeScreen.value.widgets.find(w => w.id === selectedWidgetId.value);
|
||||
});
|
||||
|
||||
function ensureButtonLabels(screen) {
|
||||
function cleanupButtonLabels(screen) {
|
||||
if (!screen || !Array.isArray(screen.widgets)) return;
|
||||
|
||||
const hasLabelChild = new Set();
|
||||
const buttonIds = new Set(
|
||||
screen.widgets.filter(w => w.type === WIDGET_TYPES.BUTTON).map(w => w.id)
|
||||
);
|
||||
if (!buttonIds.size) return;
|
||||
|
||||
const removeIds = new Set();
|
||||
screen.widgets.forEach((w) => {
|
||||
if (w.type === WIDGET_TYPES.LABEL && w.parentId !== -1) {
|
||||
hasLabelChild.add(w.parentId);
|
||||
if (w.type === WIDGET_TYPES.LABEL && buttonIds.has(w.parentId)) {
|
||||
removeIds.add(w.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (removeIds.size) {
|
||||
screen.widgets = screen.widgets.filter(w => !removeIds.has(w.id));
|
||||
}
|
||||
|
||||
// Update container flag for buttons based on remaining children
|
||||
screen.widgets.forEach((w) => {
|
||||
if (w.type !== WIDGET_TYPES.BUTTON) return;
|
||||
w.isContainer = true;
|
||||
|
||||
if (hasLabelChild.has(w.id)) return;
|
||||
|
||||
const defaults = WIDGET_DEFAULTS.label;
|
||||
const labelText = w.text || WIDGET_DEFAULTS.button.text;
|
||||
const label = {
|
||||
id: nextWidgetId.value++,
|
||||
parentId: w.id,
|
||||
type: WIDGET_TYPES.LABEL,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: w.w,
|
||||
h: w.h,
|
||||
visible: true,
|
||||
textSrc: defaults.textSrc,
|
||||
text: labelText,
|
||||
knxAddr: defaults.knxAddr,
|
||||
fontSize: w.fontSize ?? defaults.fontSize,
|
||||
textAlign: w.textAlign ?? TEXT_ALIGNS.CENTER,
|
||||
textColor: w.textColor ?? defaults.textColor,
|
||||
bgColor: defaults.bgColor,
|
||||
bgOpacity: 0,
|
||||
radius: 0,
|
||||
borderWidth: 0,
|
||||
borderColor: defaults.borderColor || '#ffffff',
|
||||
borderOpacity: 0,
|
||||
shadow: { ...defaults.shadow, enabled: false },
|
||||
isToggle: false,
|
||||
knxAddrWrite: 0,
|
||||
action: 0,
|
||||
targetScreen: 0,
|
||||
iconCodepoint: w.iconCodepoint || 0,
|
||||
iconPosition: w.iconPosition ?? defaults.iconPosition,
|
||||
iconSize: w.iconSize ?? defaults.iconSize,
|
||||
iconGap: w.iconGap ?? defaults.iconGap,
|
||||
iconPositionX: w.iconPositionX ?? defaults.iconPositionX,
|
||||
iconPositionY: w.iconPositionY ?? defaults.iconPositionY,
|
||||
};
|
||||
|
||||
screen.widgets.push(label);
|
||||
hasLabelChild.add(w.id);
|
||||
const hasChild = screen.widgets.some(child => child.parentId === w.id);
|
||||
w.isContainer = hasChild;
|
||||
});
|
||||
}
|
||||
|
||||
@ -252,7 +222,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
config.screens.forEach((screen) => {
|
||||
normalizeScreen(screen, nextScreenId, nextWidgetId);
|
||||
ensureButtonLabels(screen);
|
||||
cleanupButtonLabels(screen);
|
||||
// Also update max widget id
|
||||
screen.widgets.forEach(w => {
|
||||
nextWidgetId.value = Math.max(nextWidgetId.value, w.id + 1);
|
||||
@ -459,44 +429,6 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
activeScreen.value.widgets.push(w);
|
||||
selectedWidgetId.value = w.id;
|
||||
|
||||
if (typeStr === 'button') {
|
||||
const labelDefaults = WIDGET_DEFAULTS.label;
|
||||
const label = {
|
||||
id: nextWidgetId.value++,
|
||||
parentId: w.id,
|
||||
type: WIDGET_TYPES.LABEL,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: w.w,
|
||||
h: w.h,
|
||||
visible: true,
|
||||
textSrc: labelDefaults.textSrc,
|
||||
text: w.text,
|
||||
knxAddr: labelDefaults.knxAddr,
|
||||
fontSize: w.fontSize,
|
||||
textAlign: TEXT_ALIGNS.CENTER,
|
||||
textColor: w.textColor,
|
||||
bgColor: labelDefaults.bgColor,
|
||||
bgOpacity: 0,
|
||||
radius: 0,
|
||||
borderWidth: 0,
|
||||
borderColor: labelDefaults.borderColor || '#ffffff',
|
||||
borderOpacity: 0,
|
||||
shadow: { ...labelDefaults.shadow, enabled: false },
|
||||
isToggle: false,
|
||||
knxAddrWrite: 0,
|
||||
action: 0,
|
||||
targetScreen: 0,
|
||||
iconCodepoint: w.iconCodepoint || 0,
|
||||
iconPosition: w.iconPosition || 0,
|
||||
iconSize: w.iconSize || 1,
|
||||
iconGap: w.iconGap || 8,
|
||||
iconPositionX: w.iconPositionX || 8,
|
||||
iconPositionY: w.iconPositionY || 8
|
||||
};
|
||||
activeScreen.value.widgets.push(label);
|
||||
}
|
||||
|
||||
// Auto-create pages for new TabView
|
||||
if (typeStr === 'tabview') {
|
||||
// Deselect to reset parent logic for recursive calls, or explicitly pass parent
|
||||
@ -611,7 +543,22 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
}
|
||||
});
|
||||
|
||||
const parentIds = new Set();
|
||||
activeScreen.value.widgets.forEach((w) => {
|
||||
if (deleteIds.includes(w.id) && w.parentId !== -1) {
|
||||
parentIds.add(w.parentId);
|
||||
}
|
||||
});
|
||||
|
||||
activeScreen.value.widgets = activeScreen.value.widgets.filter(w => !deleteIds.includes(w.id));
|
||||
|
||||
parentIds.forEach((pid) => {
|
||||
const parent = activeScreen.value.widgets.find(w => w.id === pid);
|
||||
if (parent && parent.type === WIDGET_TYPES.BUTTON) {
|
||||
const hasChild = activeScreen.value.widgets.some(w => w.parentId === pid);
|
||||
parent.isContainer = hasChild;
|
||||
}
|
||||
});
|
||||
selectedWidgetId.value = null;
|
||||
}
|
||||
|
||||
@ -629,12 +576,26 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
const widget = activeScreen.value.widgets.find(w => w.id === widgetId);
|
||||
if (widget) {
|
||||
const oldParentId = widget.parentId;
|
||||
// Adjust position to be relative to new parent (simplified: reset to 0,0 or keep absolute?)
|
||||
// Keeping absolute is hard because we don't know absolute pos easily here.
|
||||
// Resetting to 10,10 is safer.
|
||||
widget.x = 10;
|
||||
widget.y = 10;
|
||||
widget.parentId = newParentId;
|
||||
|
||||
if (oldParentId !== -1) {
|
||||
const oldParent = activeScreen.value.widgets.find(w => w.id === oldParentId);
|
||||
if (oldParent && oldParent.type === WIDGET_TYPES.BUTTON) {
|
||||
const hasChild = activeScreen.value.widgets.some(w => w.parentId === oldParentId);
|
||||
oldParent.isContainer = hasChild;
|
||||
}
|
||||
}
|
||||
|
||||
const newParent = activeScreen.value.widgets.find(w => w.id === newParentId);
|
||||
if (newParent && newParent.type === WIDGET_TYPES.BUTTON) {
|
||||
newParent.isContainer = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -159,8 +159,8 @@ export function normalizeWidget(w, nextWidgetIdRef) {
|
||||
// Hierarchy
|
||||
if (w.parentId === undefined) w.parentId = -1;
|
||||
|
||||
if (key === 'button') {
|
||||
w.isContainer = true;
|
||||
if (key === 'button' && (w.isContainer === undefined || w.isContainer === null)) {
|
||||
w.isContainer = defaults.isContainer ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user