Fixes
This commit is contained in:
parent
1ea8bb7e12
commit
f7f3f8946c
@ -45,7 +45,9 @@ void WidgetConfig::serialize(uint8_t* buf) const {
|
||||
buf[pos++] = iconPosition;
|
||||
buf[pos++] = iconSize;
|
||||
buf[pos++] = static_cast<uint8_t>(iconGap);
|
||||
buf[pos++] = 0; // padding for alignment
|
||||
|
||||
// Hierarchy
|
||||
buf[pos++] = static_cast<uint8_t>(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<int8_t>(buf[pos++]);
|
||||
pos++; // padding
|
||||
|
||||
// Hierarchy
|
||||
parentId = static_cast<int8_t>(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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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_);
|
||||
}
|
||||
lv_obj_delete(modalContainer_);
|
||||
esp_lv_adapter_unlock();
|
||||
modalContainer_ = nullptr;
|
||||
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);
|
||||
}
|
||||
modalContainer_ = 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<int>(cfg.action), cfg.targetScreen, static_cast<int>(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();
|
||||
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_) {
|
||||
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) {
|
||||
@ -777,6 +869,8 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
|
||||
cJSON_AddNumberToObject(widget, "iconSize", w.iconSize);
|
||||
cJSON_AddNumberToObject(widget, "iconGap", w.iconGap);
|
||||
|
||||
cJSON_AddNumberToObject(widget, "parentId", w.parentId);
|
||||
|
||||
cJSON_AddItemToArray(widgets, widget);
|
||||
}
|
||||
|
||||
@ -924,6 +1018,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<int8_t>(parentId->valueint);
|
||||
} else {
|
||||
w.parentId = -1; // Default to root
|
||||
}
|
||||
|
||||
screen.widgetCount++;
|
||||
}
|
||||
|
||||
|
||||
@ -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<ButtonWidget*>(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_obj_t*>(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<uint8_t>(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)",
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
@click.self="deselect"
|
||||
>
|
||||
<WidgetElement
|
||||
v-for="widget in store.activeScreen?.widgets || []"
|
||||
v-for="widget in rootWidgets"
|
||||
:key="widget.id"
|
||||
:widget="widget"
|
||||
:scale="store.canvasScale"
|
||||
@ -32,16 +32,41 @@ import { clamp, minSizeFor } from '../utils';
|
||||
|
||||
const store = useEditorStore();
|
||||
|
||||
const rootWidgets = computed(() => {
|
||||
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);
|
||||
|
||||
@ -54,6 +54,19 @@
|
||||
<option :value="1">Modal</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<template v-if="store.activeScreen.mode === 1">
|
||||
<div class="control-row"><label>X</label><input type="number" v-model.number="store.activeScreen.modal.x"></div>
|
||||
<div class="control-row"><label>Y</label><input type="number" v-model.number="store.activeScreen.modal.y"></div>
|
||||
<div class="control-row"><label>Breite</label><input type="number" v-model.number="store.activeScreen.modal.w"></div>
|
||||
<div class="control-row"><label>Hoehe</label><input type="number" v-model.number="store.activeScreen.modal.h"></div>
|
||||
<div class="control-row"><label>Radius</label><input type="number" v-model.number="store.activeScreen.modal.radius"></div>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" v-model="store.activeScreen.modal.dim">
|
||||
<span>Hintergrund dimmen</span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<button class="btn ghost danger small" @click="store.deleteScreen">Screen loeschen</button>
|
||||
</div>
|
||||
</section>
|
||||
@ -66,19 +79,13 @@
|
||||
<div class="tree">
|
||||
<div class="tree-root">{{ store.activeScreen?.name }}</div>
|
||||
<div class="tree-list">
|
||||
<div v-if="!store.activeScreen?.widgets.length" class="tree-empty">Keine Widgets</div>
|
||||
<div
|
||||
<div v-if="!store.widgetTree.length" class="tree-empty">Keine Widgets</div>
|
||||
<TreeItem
|
||||
v-else
|
||||
v-for="w in store.activeScreen.widgets"
|
||||
:key="w.id"
|
||||
class="tree-item"
|
||||
:class="{ active: store.selectedWidgetId === w.id, hidden: !w.visible }"
|
||||
@click="store.selectedWidgetId = w.id"
|
||||
>
|
||||
<span class="tree-tag">{{ TYPE_LABELS[typeKeyFor(w.type)] }}</span>
|
||||
<span class="tree-name">{{ w.text || TYPE_LABELS[typeKeyFor(w.type)] }}</span>
|
||||
<span class="tree-id">#{{ w.id }}</span>
|
||||
</div>
|
||||
v-for="node in store.widgetTree"
|
||||
:key="node.id"
|
||||
:node="node"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -103,6 +110,14 @@
|
||||
<input type="checkbox" v-model="store.showGrid">
|
||||
<span>Grid anzeigen</span>
|
||||
</label>
|
||||
<div class="control-row" style="margin-top: 8px;">
|
||||
<label>Grid Gr.</label>
|
||||
<input type="number" min="5" max="100" v-model.number="store.gridSize">
|
||||
</div>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" v-model="store.snapToGrid">
|
||||
<span>Am Grid ausrichten</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
@ -136,6 +151,7 @@
|
||||
|
||||
<script setup>
|
||||
import { useEditorStore } from '../stores/editor';
|
||||
import TreeItem from './TreeItem.vue';
|
||||
import { typeKeyFor } from '../utils';
|
||||
import { TYPE_LABELS } from '../constants';
|
||||
|
||||
|
||||
95
web-interface/src/components/TreeItem.vue
Normal file
95
web-interface/src/components/TreeItem.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="tree-node">
|
||||
<div
|
||||
class="tree-item"
|
||||
:class="{ active: store.selectedWidgetId === node.id, hidden: !node.visible }"
|
||||
@click.stop="store.selectedWidgetId = node.id"
|
||||
:style="{ paddingLeft: `${level * 12 + 10}px` }"
|
||||
>
|
||||
<span class="tree-icon" v-if="node.children.length > 0">
|
||||
{{ expanded ? '▼' : '▶' }}
|
||||
</span>
|
||||
<span class="tree-tag">{{ TYPE_LABELS[typeKeyFor(node.type)] }}</span>
|
||||
<span class="tree-name">{{ node.text || TYPE_LABELS[typeKeyFor(node.type)] }}</span>
|
||||
<span class="tree-id">#{{ node.id }}</span>
|
||||
</div>
|
||||
|
||||
<div class="tree-children" v-if="node.children.length > 0">
|
||||
<TreeItem
|
||||
v-for="child in node.children"
|
||||
:key="child.id"
|
||||
:node="child"
|
||||
:level="level + 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useEditorStore } from '../stores/editor';
|
||||
import { typeKeyFor } from '../utils';
|
||||
import { TYPE_LABELS } from '../constants';
|
||||
|
||||
const props = defineProps({
|
||||
node: Object,
|
||||
level: { type: Number, default: 0 }
|
||||
});
|
||||
|
||||
const store = useEditorStore();
|
||||
const expanded = ref(true);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tree-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tree-item.active {
|
||||
border-color: var(--accent-2);
|
||||
background: rgba(125, 211, 176, 0.1);
|
||||
}
|
||||
|
||||
.tree-item.hidden {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tree-tag {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(125, 211, 176, 0.15);
|
||||
color: var(--accent-2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.tree-name {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tree-id {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.tree-icon {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
width: 12px;
|
||||
}
|
||||
</style>
|
||||
@ -20,6 +20,18 @@
|
||||
@touchstart.stop="$emit('resize-start', $event)"
|
||||
></div>
|
||||
|
||||
<!-- Recursive Children -->
|
||||
<WidgetElement
|
||||
v-for="child in children"
|
||||
:key="child.id"
|
||||
:widget="child"
|
||||
:scale="scale"
|
||||
:selected="store.selectedWidgetId === child.id"
|
||||
@select="store.selectedWidgetId = child.id"
|
||||
@drag-start="$emit('drag-start', $event)"
|
||||
@resize-start="$emit('resize-start', $event)"
|
||||
/>
|
||||
|
||||
<!-- Icon-only Widget -->
|
||||
<template v-if="isIcon">
|
||||
<span class="material-symbols-outlined icon-display" :style="iconOnlyStyle">
|
||||
@ -53,6 +65,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useEditorStore } from '../stores/editor';
|
||||
import { WIDGET_TYPES, fontSizes, ICON_POSITIONS } from '../constants';
|
||||
import { clamp, hexToRgba } from '../utils';
|
||||
|
||||
@ -62,7 +75,14 @@ const props = defineProps({
|
||||
selected: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
defineEmits(['select', 'drag-start', 'resize-start']);
|
||||
const emit = defineEmits(['select', 'drag-start', 'resize-start']);
|
||||
|
||||
const store = useEditorStore();
|
||||
|
||||
const children = computed(() => {
|
||||
if (!store.activeScreen) return [];
|
||||
return store.activeScreen.widgets.filter(w => w.parentId === props.widget.id);
|
||||
});
|
||||
|
||||
const isLabel = computed(() => props.widget.type === WIDGET_TYPES.LABEL);
|
||||
const isButton = computed(() => props.widget.type === WIDGET_TYPES.BUTTON);
|
||||
|
||||
@ -15,6 +15,8 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
const activeScreenId = ref(0);
|
||||
const canvasScale = ref(0.6);
|
||||
const showGrid = ref(true);
|
||||
const snapToGrid = ref(true);
|
||||
const gridSize = ref(20);
|
||||
|
||||
const nextScreenId = ref(0);
|
||||
const nextWidgetId = ref(0);
|
||||
@ -23,6 +25,27 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
return config.screens.find(s => s.id === activeScreenId.value) || config.screens[0];
|
||||
});
|
||||
|
||||
const widgetTree = computed(() => {
|
||||
if (!activeScreen.value) return [];
|
||||
const widgets = activeScreen.value.widgets;
|
||||
|
||||
// Map ID -> Widget
|
||||
const map = {};
|
||||
widgets.forEach(w => map[w.id] = { ...w, children: [] });
|
||||
|
||||
const roots = [];
|
||||
widgets.forEach(w => {
|
||||
const node = map[w.id];
|
||||
if (w.parentId !== -1 && map[w.parentId]) {
|
||||
map[w.parentId].children.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
});
|
||||
|
||||
const selectedWidget = computed(() => {
|
||||
if (!activeScreen.value || selectedWidgetId.value === null) return null;
|
||||
return activeScreen.value.widgets.find(w => w.id === selectedWidgetId.value);
|
||||
@ -133,6 +156,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
bgColor: '#1A1A2E',
|
||||
widgets: []
|
||||
};
|
||||
normalizeScreen(newScreen, null, nextWidgetId);
|
||||
config.screens.push(newScreen);
|
||||
activeScreenId.value = id;
|
||||
selectedWidgetId.value = null;
|
||||
@ -168,11 +192,27 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
}
|
||||
const defaults = WIDGET_DEFAULTS[typeStr];
|
||||
|
||||
// Determine parent
|
||||
let parentId = -1;
|
||||
let startX = 120;
|
||||
let startY = 120;
|
||||
|
||||
if (selectedWidgetId.value !== null) {
|
||||
const parent = activeScreen.value.widgets.find(w => w.id === selectedWidgetId.value);
|
||||
// Only BUTTON can be a parent for now (as a container)
|
||||
if (parent && parent.type === WIDGET_TYPES.BUTTON) {
|
||||
parentId = parent.id;
|
||||
startX = 10; // Relative to parent
|
||||
startY = 10;
|
||||
}
|
||||
}
|
||||
|
||||
const w = {
|
||||
id: nextWidgetId.value++,
|
||||
parentId: parentId,
|
||||
type: typeValue,
|
||||
x: 120,
|
||||
y: 120,
|
||||
x: startX,
|
||||
y: startY,
|
||||
w: defaults.w,
|
||||
h: defaults.h,
|
||||
visible: true,
|
||||
@ -212,7 +252,10 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
activeScreenId,
|
||||
canvasScale,
|
||||
showGrid,
|
||||
snapToGrid,
|
||||
gridSize,
|
||||
activeScreen,
|
||||
widgetTree,
|
||||
selectedWidget,
|
||||
loadKnxAddresses,
|
||||
loadConfig,
|
||||
|
||||
@ -412,10 +412,10 @@ body {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(255, 255, 255, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.06) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
opacity: 0.3;
|
||||
linear-gradient(to right, rgba(255, 255, 255, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
|
||||
background-size: var(--grid-size, 32px) var(--grid-size, 32px);
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@ -60,6 +60,9 @@ export function normalizeWidget(w, nextWidgetIdRef) {
|
||||
if (w.iconPosition === undefined) w.iconPosition = defaults.iconPosition || 0;
|
||||
if (w.iconSize === undefined) w.iconSize = defaults.iconSize || 1;
|
||||
if (w.iconGap === undefined) w.iconGap = defaults.iconGap || 8;
|
||||
|
||||
// Hierarchy
|
||||
if (w.parentId === undefined) w.parentId = -1;
|
||||
}
|
||||
|
||||
export function normalizeScreen(screen, nextScreenIdRef, nextWidgetIdRef) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user