From f7f3f8946c91f59a9894e6cf0aef6938f514ec95 Mon Sep 17 00:00:00 2001 From: Thomas Peterson Date: Sun, 25 Jan 2026 21:24:40 +0100 Subject: [PATCH] Fixes --- main/WidgetConfig.cpp | 10 +- main/WidgetConfig.hpp | 5 +- main/WidgetManager.cpp | 137 +++++++++++++++--- main/widgets/ButtonWidget.cpp | 14 +- main/widgets/Widget.hpp | 1 + web-interface/src/components/CanvasArea.vue | 73 ++++++++-- web-interface/src/components/SidebarLeft.vue | 40 +++-- web-interface/src/components/TreeItem.vue | 95 ++++++++++++ .../src/components/WidgetElement.vue | 22 ++- web-interface/src/stores/editor.js | 47 +++++- web-interface/src/style.css | 8 +- web-interface/src/utils.js | 3 + 12 files changed, 403 insertions(+), 52 deletions(-) create mode 100644 web-interface/src/components/TreeItem.vue diff --git a/main/WidgetConfig.cpp b/main/WidgetConfig.cpp index 3de9483..2f54c85 100644 --- a/main/WidgetConfig.cpp +++ b/main/WidgetConfig.cpp @@ -45,7 +45,9 @@ void WidgetConfig::serialize(uint8_t* buf) const { buf[pos++] = iconPosition; buf[pos++] = iconSize; buf[pos++] = static_cast(iconGap); - buf[pos++] = 0; // padding for alignment + + // Hierarchy + buf[pos++] = static_cast(parentId); } void WidgetConfig::deserialize(const uint8_t* buf) { @@ -89,12 +91,15 @@ void WidgetConfig::deserialize(const uint8_t* buf) { iconPosition = buf[pos++]; iconSize = buf[pos++]; iconGap = static_cast(buf[pos++]); - pos++; // padding + + // Hierarchy + parentId = static_cast(buf[pos++]); } WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const char* labelText) { WidgetConfig cfg = {}; cfg.id = id; + cfg.parentId = -1; // Root cfg.type = WidgetType::LABEL; cfg.x = x; cfg.y = y; @@ -129,6 +134,7 @@ WidgetConfig WidgetConfig::createButton(uint8_t id, int16_t x, int16_t y, const char* labelText, uint16_t knxAddrWrite, bool toggle) { WidgetConfig cfg = {}; cfg.id = id; + cfg.parentId = -1; // Root cfg.type = WidgetType::BUTTON; cfg.x = x; cfg.y = y; diff --git a/main/WidgetConfig.hpp b/main/WidgetConfig.hpp index 9cc021e..5e0e980 100644 --- a/main/WidgetConfig.hpp +++ b/main/WidgetConfig.hpp @@ -107,8 +107,11 @@ struct WidgetConfig { uint8_t iconSize; // Font size index (0-5), same as fontSize int8_t iconGap; // Gap between icon and text (px) + // Hierarchy + int8_t parentId; // ID of parent widget (-1 = root/screen) + // Serialization size (fixed for NVS storage) - static constexpr size_t SERIALIZED_SIZE = 76; + static constexpr size_t SERIALIZED_SIZE = 77; void serialize(uint8_t* buf) const; void deserialize(const uint8_t* buf); diff --git a/main/WidgetManager.cpp b/main/WidgetManager.cpp index 40e7c86..6bcca13 100644 --- a/main/WidgetManager.cpp +++ b/main/WidgetManager.cpp @@ -388,28 +388,46 @@ void WidgetManager::showModalScreen(const ScreenConfig& screen) { } void WidgetManager::closeModal() { - if (!modalContainer_) return; + printf("WM: closeModal Start. Container=%p\n", (void*)modalContainer_); + fflush(stdout); + if (!modalContainer_) { + return; + } - if (esp_lv_adapter_lock(-1) != ESP_OK) return; + if (esp_lv_adapter_lock(-1) != ESP_OK) { + ESP_LOGE(TAG, "closeModal: Failed to lock LVGL"); + return; + } - // Reset all input devices BEFORE destroying widgets + // Reset input devices lv_indev_t* indev = lv_indev_get_next(nullptr); while (indev) { lv_indev_reset(indev, nullptr); indev = lv_indev_get_next(indev); } - // First destroy C++ widgets (which deletes their LVGL objects) + // Destroy widgets first + printf("WM: closeModal destroying widgets...\n"); + fflush(stdout); destroyAllWidgets(); if (modalDimmer_) { - lv_obj_delete(modalDimmer_); + if (lv_obj_is_valid(modalDimmer_)) lv_obj_delete(modalDimmer_); + modalDimmer_ = nullptr; + } + + if (lv_obj_is_valid(modalContainer_)) { + lv_obj_delete(modalContainer_); + } else { + printf("WM: Warning: modalContainer_ was invalid!\n"); + fflush(stdout); } - lv_obj_delete(modalContainer_); - esp_lv_adapter_unlock(); modalContainer_ = nullptr; - modalDimmer_ = nullptr; modalScreenId_ = SCREEN_ID_NONE; + + esp_lv_adapter_unlock(); + printf("WM: closeModal Complete\n"); + fflush(stdout); } void WidgetManager::showScreen(uint8_t screenId) { @@ -436,10 +454,12 @@ void WidgetManager::showScreen(uint8_t screenId) { } void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target) { - ESP_LOGI(TAG, "handleButtonAction: button=%d action=%d targetScreen=%d type=%d", - cfg.id, static_cast(cfg.action), cfg.targetScreen, static_cast(cfg.type)); + printf("WM: handleButtonAction btn=%d act=%d type=%d\n", cfg.id, (int)cfg.action, (int)cfg.type); + fflush(stdout); if (cfg.type != WidgetType::BUTTON) { + printf("WM: Not a button!\n"); + fflush(stdout); return; } @@ -447,14 +467,16 @@ void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target switch (cfg.action) { case ButtonAction::JUMP: - ESP_LOGI(TAG, "JUMP action: scheduling navigation to screen %d", cfg.targetScreen); + printf("WM: Action JUMP to %d\n", cfg.targetScreen); + fflush(stdout); navAction_ = ButtonAction::JUMP; navTargetScreen_ = cfg.targetScreen; navPending_ = true; navRequestUs_ = esp_timer_get_time(); break; case ButtonAction::BACK: - ESP_LOGI(TAG, "BACK action: scheduling navigation back"); + printf("WM: Action BACK\n"); + fflush(stdout); navAction_ = ButtonAction::BACK; navTargetScreen_ = SCREEN_ID_NONE; navPending_ = true; @@ -477,16 +499,43 @@ void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target } void WidgetManager::goBack() { + printf("WM: goBack called. Modal=%p Active=%d Prev=%d\n", + (void*)modalContainer_, activeScreenId_, previousScreenId_); + fflush(stdout); + if (modalContainer_) { + printf("WM: Closing modal...\n"); + fflush(stdout); closeModal(); - applyScreen(activeScreenId_); + printf("WM: Modal closed. Restoring screen %d\n", activeScreenId_); + fflush(stdout); + // Restore the active screen (which was in background) + if (config_.findScreen(activeScreenId_)) { + applyScreen(activeScreenId_); + } else { + ESP_LOGE(TAG, "Active screen %d not found after closing modal!", activeScreenId_); + if (config_.findScreen(config_.startScreenId)) { + activeScreenId_ = config_.startScreenId; + applyScreen(activeScreenId_); + } + } return; } if (previousScreenId_ != SCREEN_ID_NONE && previousScreenId_ != activeScreenId_) { - activeScreenId_ = previousScreenId_; - previousScreenId_ = SCREEN_ID_NONE; - applyScreen(activeScreenId_); + printf("WM: Going back to screen %d\n", previousScreenId_); + fflush(stdout); + if (config_.findScreen(previousScreenId_)) { + activeScreenId_ = previousScreenId_; + previousScreenId_ = SCREEN_ID_NONE; + applyScreen(activeScreenId_); + } else { + ESP_LOGW(TAG, "Previous screen %d not found", previousScreenId_); + previousScreenId_ = SCREEN_ID_NONE; + } + } else { + printf("WM: No previous screen to go back to\n"); + fflush(stdout); } } @@ -562,9 +611,14 @@ void WidgetManager::onUserActivity() { } void WidgetManager::destroyAllWidgets() { - for (auto& widget : widgets_) { - widget.reset(); + ESP_LOGI(TAG, "destroyAllWidgets: Start"); + for (size_t i = 0; i < widgets_.size(); i++) { + if (widgets_[i]) { + // ESP_LOGI(TAG, "Destroying widget %d", i); + widgets_[i].reset(); + } } + ESP_LOGI(TAG, "destroyAllWidgets: Complete"); } void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent) { @@ -576,8 +630,11 @@ void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* paren screen.backgroundColor.b), 0); lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0); + // Pass 1: Create root widgets (parentId == -1) for (uint8_t i = 0; i < screen.widgetCount; i++) { const WidgetConfig& cfg = screen.widgets[i]; + if (cfg.parentId != -1) continue; + auto widget = WidgetFactory::create(cfg); if (widget && cfg.id < MAX_WIDGETS) { widget->create(parent); @@ -585,6 +642,41 @@ void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* paren widgets_[cfg.id] = std::move(widget); } } + + // Pass 2: Create child widgets + // Simple 1-level depth support for now. For deeper nesting, we'd need a topological sort or multiple passes. + bool madeProgress = true; + int remainingPasses = 10; // Prevent infinite loops + + while (madeProgress && remainingPasses > 0) { + madeProgress = false; + remainingPasses--; + + for (uint8_t i = 0; i < screen.widgetCount; i++) { + const WidgetConfig& cfg = screen.widgets[i]; + + // Skip if already created + if (widgets_[cfg.id]) continue; + + // Skip if it's a root widget (should be created in Pass 1, but if failed/skipped, ignore) + if (cfg.parentId == -1) continue; + + // Check if parent exists + if (cfg.parentId >= 0 && cfg.parentId < MAX_WIDGETS && widgets_[cfg.parentId]) { + // Parent exists! Get its LVGL object + lv_obj_t* parentObj = widgets_[cfg.parentId]->getObj(); + if (parentObj) { + auto widget = WidgetFactory::create(cfg); + if (widget && cfg.id < MAX_WIDGETS) { + widget->create(parentObj); + widget->applyStyle(); + widgets_[cfg.id] = std::move(widget); + madeProgress = true; + } + } + } + } + } } void WidgetManager::onKnxValue(uint16_t groupAddr, float value) { @@ -776,6 +868,8 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { cJSON_AddNumberToObject(widget, "iconPosition", w.iconPosition); cJSON_AddNumberToObject(widget, "iconSize", w.iconSize); cJSON_AddNumberToObject(widget, "iconGap", w.iconGap); + + cJSON_AddNumberToObject(widget, "parentId", w.parentId); cJSON_AddItemToArray(widgets, widget); } @@ -923,6 +1017,13 @@ bool WidgetManager::updateConfigFromJson(const char* json) { cJSON* iconGap = cJSON_GetObjectItem(widget, "iconGap"); if (cJSON_IsNumber(iconGap)) w.iconGap = iconGap->valueint; + + cJSON* parentId = cJSON_GetObjectItem(widget, "parentId"); + if (cJSON_IsNumber(parentId)) { + w.parentId = static_cast(parentId->valueint); + } else { + w.parentId = -1; // Default to root + } screen.widgetCount++; } diff --git a/main/widgets/ButtonWidget.cpp b/main/widgets/ButtonWidget.cpp index 98905c7..1c7dd14 100644 --- a/main/widgets/ButtonWidget.cpp +++ b/main/widgets/ButtonWidget.cpp @@ -52,9 +52,14 @@ int ButtonWidget::encodeUtf8(uint32_t codepoint, char* buf) { } void ButtonWidget::clickCallback(lv_event_t* e) { - ESP_LOGI(TAG, "clickCallback called"); + printf("ButtonWidget::clickCallback\n"); + fflush(stdout); ButtonWidget* widget = static_cast(lv_event_get_user_data(e)); - if (!widget) return; + if (!widget) { + printf("ButtonWidget: Widget is null!\n"); + fflush(stdout); + return; + } lv_obj_t* target = static_cast(lv_event_get_target(e)); WidgetManager::instance().handleButtonAction(widget->getConfig(), target); } @@ -92,6 +97,7 @@ lv_obj_t* ButtonWidget::create(lv_obj_t* parent) { lv_obj_remove_style_all(contentContainer_); lv_obj_set_size(contentContainer_, LV_SIZE_CONTENT, LV_SIZE_CONTENT); lv_obj_center(contentContainer_); + lv_obj_clear_flag(contentContainer_, LV_OBJ_FLAG_CLICKABLE); // Pass clicks to parent // Create icon label bool iconFirst = (config_.iconPosition == static_cast(IconPosition::LEFT) || @@ -102,17 +108,20 @@ lv_obj_t* ButtonWidget::create(lv_obj_t* parent) { char iconText[5]; encodeUtf8(config_.iconCodepoint, iconText); lv_label_set_text(iconLabel_, iconText); + lv_obj_clear_flag(iconLabel_, LV_OBJ_FLAG_CLICKABLE); } // Create text label label_ = lv_label_create(contentContainer_); lv_label_set_text(label_, config_.text); + lv_obj_clear_flag(label_, LV_OBJ_FLAG_CLICKABLE); if (!iconFirst) { iconLabel_ = lv_label_create(contentContainer_); char iconText[5]; encodeUtf8(config_.iconCodepoint, iconText); lv_label_set_text(iconLabel_, iconText); + lv_obj_clear_flag(iconLabel_, LV_OBJ_FLAG_CLICKABLE); } setupFlexLayout(); @@ -121,6 +130,7 @@ lv_obj_t* ButtonWidget::create(lv_obj_t* parent) { label_ = lv_label_create(obj_); lv_label_set_text(label_, config_.text); lv_obj_center(label_); + lv_obj_clear_flag(label_, LV_OBJ_FLAG_CLICKABLE); } ESP_LOGI(TAG, "Created button '%s' at %d,%d (icon: 0x%lx)", diff --git a/main/widgets/Widget.hpp b/main/widgets/Widget.hpp index d975d0d..7f44f38 100644 --- a/main/widgets/Widget.hpp +++ b/main/widgets/Widget.hpp @@ -17,6 +17,7 @@ public: // Access to LVGL object lv_obj_t* getLvglObject() const { return obj_; } + lv_obj_t* getObj() const { return obj_; } // Alias // Widget ID uint8_t getId() const { return config_.id; } diff --git a/web-interface/src/components/CanvasArea.vue b/web-interface/src/components/CanvasArea.vue index a743179..872fea6 100644 --- a/web-interface/src/components/CanvasArea.vue +++ b/web-interface/src/components/CanvasArea.vue @@ -9,7 +9,7 @@ @click.self="deselect" > { + return store.activeScreen?.widgets.filter(w => w.parentId === -1) || []; +}); + +const canvasW = computed(() => { + if (store.activeScreen?.mode === 1) { // Modal + return store.activeScreen.modal.w || DISPLAY_W; + } + return DISPLAY_W; +}); + +const canvasH = computed(() => { + if (store.activeScreen?.mode === 1) { // Modal + return store.activeScreen.modal.h || DISPLAY_H; + } + return DISPLAY_H; +}); + const canvasStyle = computed(() => ({ - width: `${DISPLAY_W * store.canvasScale}px`, - height: `${DISPLAY_H * store.canvasScale}px`, - '--canvas-bg': store.activeScreen?.bgColor || '#1A1A2E' + width: `${canvasW.value * store.canvasScale}px`, + height: `${canvasH.value * store.canvasScale}px`, + '--canvas-bg': store.activeScreen?.bgColor || '#1A1A2E', + '--grid-size': `${store.gridSize * store.canvasScale}px` })); function deselect() { store.selectedWidgetId = null; } +function snap(val) { + if (!store.snapToGrid) return val; + const s = store.gridSize; + return Math.round(val / s) * s; +} + let dragState = null; let resizeState = null; @@ -83,8 +108,28 @@ function drag(e) { const w = store.activeScreen.widgets.find(x => x.id === dragState.id); if (w) { - w.x = Math.max(0, Math.min(DISPLAY_W - w.w, Math.round(dragState.origX + dx))); - w.y = Math.max(0, Math.min(DISPLAY_H - w.h, Math.round(dragState.origY + dy))); + let nx = dragState.origX + dx; + let ny = dragState.origY + dy; + + if (store.snapToGrid) { + nx = snap(nx); + ny = snap(ny); + } + + // Determine bounds + let maxW = canvasW.value; + let maxH = canvasH.value; + + if (w.parentId !== -1) { + const parent = store.activeScreen.widgets.find(p => p.id === w.parentId); + if (parent) { + maxW = parent.w; + maxH = parent.h; + } + } + + w.x = Math.max(0, Math.min(maxW - w.w, Math.round(nx))); + w.y = Math.max(0, Math.min(maxH - w.h, Math.round(ny))); } } @@ -135,11 +180,19 @@ function resizeDrag(e) { if (!w) return; const minSize = minSizeFor(w); - const maxW = DISPLAY_W - w.x; - const maxH = DISPLAY_H - w.y; + const maxW = canvasW.value - w.x; + const maxH = canvasH.value - w.y; - let newW = Math.round(resizeState.origW + dx); - let newH = Math.round(resizeState.origH + dy); + let rawW = resizeState.origW + dx; + let rawH = resizeState.origH + dy; + + if (store.snapToGrid) { + rawW = snap(rawW); + rawH = snap(rawH); + } + + let newW = Math.round(rawW); + let newH = Math.round(rawH); if (w.type === WIDGET_TYPES.LED) { const maxSize = Math.min(maxW, maxH); diff --git a/web-interface/src/components/SidebarLeft.vue b/web-interface/src/components/SidebarLeft.vue index 6475ab0..cbeb645 100644 --- a/web-interface/src/components/SidebarLeft.vue +++ b/web-interface/src/components/SidebarLeft.vue @@ -54,6 +54,19 @@ + + + @@ -66,19 +79,13 @@
{{ store.activeScreen?.name }}
-
Keine Widgets
-
Keine Widgets
+ - {{ TYPE_LABELS[typeKeyFor(w.type)] }} - {{ w.text || TYPE_LABELS[typeKeyFor(w.type)] }} - #{{ w.id }} -
+ v-for="node in store.widgetTree" + :key="node.id" + :node="node" + />
@@ -103,6 +110,14 @@ Grid anzeigen +
+ + +
+
@@ -136,6 +151,7 @@ + + \ No newline at end of file diff --git a/web-interface/src/components/WidgetElement.vue b/web-interface/src/components/WidgetElement.vue index f5f1201..e477468 100644 --- a/web-interface/src/components/WidgetElement.vue +++ b/web-interface/src/components/WidgetElement.vue @@ -20,6 +20,18 @@ @touchstart.stop="$emit('resize-start', $event)" > + + +