Files
Comfyui-LayerForge/js/CanvasLayersPanel.js
Dariusz L 29ab916759 Refactor layers panel UI and improve resize handling
Changed UI text in CanvasLayersPanel from Polish to English and removed the add layer button and its logic. Moved and improved the ResizeObserver logic in CanvasView.js to update both the canvas and layers panel positions dynamically based on the controls' height.
2025-07-02 09:40:21 +02:00

676 lines
21 KiB
JavaScript

import {createModuleLogger} from "./utils/LoggerUtils.js";
const log = createModuleLogger('CanvasLayersPanel');
export class CanvasLayersPanel {
constructor(canvas) {
this.canvas = canvas;
this.container = null;
this.layersContainer = null;
this.draggedElements = [];
this.dragInsertionLine = null;
this.isMultiSelecting = false;
this.lastSelectedIndex = -1;
// Binding metod dla event handlerów
this.handleLayerClick = this.handleLayerClick.bind(this);
this.handleDragStart = this.handleDragStart.bind(this);
this.handleDragOver = this.handleDragOver.bind(this);
this.handleDragEnd = this.handleDragEnd.bind(this);
this.handleDrop = this.handleDrop.bind(this);
log.info('CanvasLayersPanel initialized');
}
/**
* Tworzy strukturê HTML panelu warstw
*/
createPanelStructure() {
// Główny kontener panelu
this.container = document.createElement('div');
this.container.className = 'layers-panel';
this.container.tabIndex = 0; // Umożliwia fokus na panelu
this.container.innerHTML = `
<div class="layers-panel-header">
<span class="layers-panel-title">Layers</span>
<div class="layers-panel-controls">
<button class="layers-btn" id="delete-layer-btn" title="Delete layer">🗑</button>
</div>
</div>
<div class="layers-container" id="layers-container">
<!-- Lista warstw będzie renderowana tutaj -->
</div>
`;
this.layersContainer = this.container.querySelector('#layers-container');
// Dodanie stylów CSS
this.injectStyles();
// Setup event listeners dla przycisków
this.setupControlButtons();
// Dodaj listener dla klawiatury, aby usuwanie działało z panelu
this.container.addEventListener('keydown', (e) => {
// Przekaż zdarzenie do głównego handlera interakcji
if (this.canvas.canvasInteractions) {
this.canvas.canvasInteractions.handleKeyDown(e);
}
});
log.debug('Panel structure created');
return this.container;
}
/**
* Dodaje style CSS do panelu
*/
injectStyles() {
const styleId = 'layers-panel-styles';
if (document.getElementById(styleId)) {
return; // Style już istnieją
}
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
.layers-panel {
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 4px;
padding: 8px;
height: 100%;
overflow: hidden;
font-family: Arial, sans-serif;
font-size: 12px;
color: #ffffff;
user-select: none;
display: flex;
flex-direction: column;
}
.layers-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 8px;
border-bottom: 1px solid #3a3a3a;
margin-bottom: 8px;
}
.layers-panel-title {
font-weight: bold;
color: #ffffff;
}
.layers-panel-controls {
display: flex;
gap: 4px;
}
.layers-btn {
background: #3a3a3a;
border: 1px solid #4a4a4a;
color: #ffffff;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
}
.layers-btn:hover {
background: #4a4a4a;
}
.layers-btn:active {
background: #5a5a5a;
}
.layers-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.layer-row {
display: flex;
align-items: center;
padding: 6px 4px;
margin-bottom: 2px;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.15s ease;
position: relative;
gap: 6px;
}
.layer-row:hover {
background: rgba(255, 255, 255, 0.05);
}
.layer-row.selected {
background: #2d5aa0 !important;
box-shadow: inset 0 0 0 1px #4a7bc8;
}
.layer-row.dragging {
opacity: 0.6;
}
.layer-thumbnail {
width: 48px;
height: 48px;
border: 1px solid #4a4a4a;
border-radius: 2px;
background: transparent;
position: relative;
flex-shrink: 0;
overflow: hidden;
}
.layer-thumbnail canvas {
width: 100%;
height: 100%;
display: block;
}
.layer-thumbnail::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
linear-gradient(45deg, #555 25%, transparent 25%),
linear-gradient(-45deg, #555 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #555 75%),
linear-gradient(-45deg, transparent 75%, #555 75%);
background-size: 8px 8px;
background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
z-index: 1;
}
.layer-thumbnail canvas {
position: relative;
z-index: 2;
}
.layer-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 2px 4px;
border-radius: 2px;
color: #ffffff;
}
.layer-name.editing {
background: #4a4a4a;
border: 1px solid #6a6a6a;
outline: none;
color: #ffffff;
}
.layer-name input {
background: transparent;
border: none;
color: #ffffff;
font-size: 12px;
width: 100%;
outline: none;
}
.drag-insertion-line {
position: absolute;
left: 0;
right: 0;
height: 2px;
background: #4a7bc8;
border-radius: 1px;
z-index: 1000;
box-shadow: 0 0 4px rgba(74, 123, 200, 0.6);
}
.layers-container::-webkit-scrollbar {
width: 6px;
}
.layers-container::-webkit-scrollbar-track {
background: #2a2a2a;
}
.layers-container::-webkit-scrollbar-thumb {
background: #4a4a4a;
border-radius: 3px;
}
.layers-container::-webkit-scrollbar-thumb:hover {
background: #5a5a5a;
}
`;
document.head.appendChild(style);
log.debug('Styles injected');
}
/**
* Konfiguruje event listenery dla przycisków kontrolnych
*/
setupControlButtons() {
const deleteBtn = this.container.querySelector('#delete-layer-btn');
deleteBtn?.addEventListener('click', () => {
log.info('Delete layer button clicked');
this.deleteSelectedLayers();
});
}
/**
* Renderuje listę warstw
*/
renderLayers() {
if (!this.layersContainer) {
log.warn('Layers container not initialized');
return;
}
// Wyczyść istniejącą zawartość
this.layersContainer.innerHTML = '';
// Usuń linię wstawiania jeśli istnieje
this.removeDragInsertionLine();
// Sortuj warstwy według zIndex (od najwyższej do najniższej)
const sortedLayers = [...this.canvas.layers].sort((a, b) => b.zIndex - a.zIndex);
sortedLayers.forEach((layer, index) => {
const layerElement = this.createLayerElement(layer, index);
this.layersContainer.appendChild(layerElement);
});
log.debug(`Rendered ${sortedLayers.length} layers`);
}
/**
* Tworzy element HTML dla pojedynczej warstwy
*/
createLayerElement(layer, index) {
const layerRow = document.createElement('div');
layerRow.className = 'layer-row';
layerRow.draggable = true;
layerRow.dataset.layerIndex = index;
// Sprawdź czy warstwa jest zaznaczona
const isSelected = this.canvas.selectedLayers.includes(layer);
if (isSelected) {
layerRow.classList.add('selected');
}
// Ustawienie domyślnych właściwości jeśli nie istnieją
if (!layer.name) {
layer.name = this.ensureUniqueName(`Layer ${layer.zIndex + 1}`, layer);
} else {
// Sprawdź unikalność istniejącej nazwy (np. przy duplikowaniu)
layer.name = this.ensureUniqueName(layer.name, layer);
}
layerRow.innerHTML = `
<div class="layer-thumbnail" data-layer-index="${index}"></div>
<span class="layer-name" data-layer-index="${index}">${layer.name}</span>
`;
// Wygeneruj miniaturkę
this.generateThumbnail(layer, layerRow.querySelector('.layer-thumbnail'));
// Event listenery
this.setupLayerEventListeners(layerRow, layer, index);
return layerRow;
}
/**
* Generuje miniaturkę warstwy
*/
generateThumbnail(layer, thumbnailContainer) {
if (!layer.image) {
thumbnailContainer.style.background = '#4a4a4a';
return;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = 48;
canvas.height = 48;
// Oblicz skalę zachowując proporcje
const scale = Math.min(48 / layer.image.width, 48 / layer.image.height);
const scaledWidth = layer.image.width * scale;
const scaledHeight = layer.image.height * scale;
// Wycentruj obraz
const x = (48 - scaledWidth) / 2;
const y = (48 - scaledHeight) / 2;
// Narysuj obraz z wyższą jakością
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(layer.image, x, y, scaledWidth, scaledHeight);
thumbnailContainer.appendChild(canvas);
}
/**
* Konfiguruje event listenery dla elementu warstwy
*/
setupLayerEventListeners(layerRow, layer, index) {
// Mousedown handler - zaznaczanie w momencie wciśnięcia przycisku
layerRow.addEventListener('mousedown', (e) => {
// Ignoruj, jeśli edytujemy nazwę
const nameElement = layerRow.querySelector('.layer-name');
if (nameElement && nameElement.classList.contains('editing')) {
return;
}
this.handleLayerClick(e, layer, index);
});
// Double click handler - edycja nazwy
layerRow.addEventListener('dblclick', (e) => {
e.preventDefault();
e.stopPropagation();
const nameElement = layerRow.querySelector('.layer-name');
this.startEditingLayerName(nameElement, layer);
});
// Drag handlers
layerRow.addEventListener('dragstart', (e) => this.handleDragStart(e, layer, index));
layerRow.addEventListener('dragover', this.handleDragOver);
layerRow.addEventListener('dragend', this.handleDragEnd);
layerRow.addEventListener('drop', (e) => this.handleDrop(e, index));
}
/**
* Obsługuje kliknięcie na warstwę, aktualizując stan bez pełnego renderowania.
*/
handleLayerClick(e, layer, index) {
const isCtrlPressed = e.ctrlKey || e.metaKey;
const isShiftPressed = e.shiftKey;
// Aktualizuj wewnętrzny stan zaznaczenia w obiekcie canvas
// Ta funkcja NIE powinna już wywoływać onSelectionChanged w panelu.
this.canvas.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index);
// Aktualizuj tylko wygląd (klasy CSS), bez niszczenia DOM
this.updateSelectionAppearance();
log.debug(`Layer clicked: ${layer.name}, selection count: ${this.canvas.selectedLayers.length}`);
}
/**
* Rozpoczyna edycję nazwy warstwy
*/
startEditingLayerName(nameElement, layer) {
const currentName = layer.name;
nameElement.classList.add('editing');
const input = document.createElement('input');
input.type = 'text';
input.value = currentName;
input.style.width = '100%';
nameElement.innerHTML = '';
nameElement.appendChild(input);
input.focus();
input.select();
const finishEditing = () => {
let newName = input.value.trim() || `Layer ${layer.zIndex + 1}`;
newName = this.ensureUniqueName(newName, layer);
layer.name = newName;
nameElement.classList.remove('editing');
nameElement.textContent = newName;
this.canvas.saveState();
log.info(`Layer renamed to: ${newName}`);
};
input.addEventListener('blur', finishEditing);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
finishEditing();
} else if (e.key === 'Escape') {
nameElement.classList.remove('editing');
nameElement.textContent = currentName;
}
});
}
/**
* Zapewnia unikalność nazwy warstwy
*/
ensureUniqueName(proposedName, currentLayer) {
const existingNames = this.canvas.layers
.filter(layer => layer !== currentLayer)
.map(layer => layer.name);
if (!existingNames.includes(proposedName)) {
return proposedName;
}
// Sprawdź czy nazwa już ma numerację w nawiasach
const match = proposedName.match(/^(.+?)\s*\((\d+)\)$/);
let baseName, startNumber;
if (match) {
baseName = match[1].trim();
startNumber = parseInt(match[2]) + 1;
} else {
baseName = proposedName;
startNumber = 1;
}
// Znajdź pierwszą dostępną numerację
let counter = startNumber;
let uniqueName;
do {
uniqueName = `${baseName} (${counter})`;
counter++;
} while (existingNames.includes(uniqueName));
return uniqueName;
}
/**
* Usuwa zaznaczone warstwy
*/
deleteSelectedLayers() {
if (this.canvas.selectedLayers.length === 0) {
log.debug('No layers selected for deletion');
return;
}
log.info(`Deleting ${this.canvas.selectedLayers.length} selected layers`);
this.canvas.removeSelectedLayers();
this.renderLayers();
}
/**
* Rozpoczyna przeciąganie warstwy
*/
handleDragStart(e, layer, index) {
// Sprawdź czy jakakolwiek warstwa jest w trybie edycji
const editingElement = this.layersContainer.querySelector('.layer-name.editing');
if (editingElement) {
e.preventDefault();
return;
}
// Jeśli przeciągana warstwa nie jest zaznaczona, zaznacz ją
if (!this.canvas.selectedLayers.includes(layer)) {
this.canvas.updateSelection([layer]);
this.renderLayers();
}
this.draggedElements = [...this.canvas.selectedLayers];
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', ''); // Wymagane przez standard
// Dodaj klasę dragging do przeciąganych elementów
this.layersContainer.querySelectorAll('.layer-row').forEach((row, idx) => {
const sortedLayers = [...this.canvas.layers].sort((a, b) => b.zIndex - a.zIndex);
if (this.draggedElements.includes(sortedLayers[idx])) {
row.classList.add('dragging');
}
});
log.debug(`Started dragging ${this.draggedElements.length} layers`);
}
/**
* Obsługuje przeciąganie nad warstwą
*/
handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const layerRow = e.currentTarget;
const rect = layerRow.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
const isUpperHalf = e.clientY < midpoint;
this.showDragInsertionLine(layerRow, isUpperHalf);
}
/**
* Pokazuje linię wskaźnika wstawiania
*/
showDragInsertionLine(targetRow, isUpperHalf) {
this.removeDragInsertionLine();
const line = document.createElement('div');
line.className = 'drag-insertion-line';
if (isUpperHalf) {
line.style.top = '-1px';
} else {
line.style.bottom = '-1px';
}
targetRow.style.position = 'relative';
targetRow.appendChild(line);
this.dragInsertionLine = line;
}
/**
* Usuwa linię wskaźnika wstawiania
*/
removeDragInsertionLine() {
if (this.dragInsertionLine) {
this.dragInsertionLine.remove();
this.dragInsertionLine = null;
}
}
/**
* Obsługuje upuszczenie warstwy
*/
handleDrop(e, targetIndex) {
e.preventDefault();
this.removeDragInsertionLine();
if (this.draggedElements.length === 0) return;
const rect = e.currentTarget.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
const isUpperHalf = e.clientY < midpoint;
// Oblicz docelowy indeks
let insertIndex = targetIndex;
if (!isUpperHalf) {
insertIndex = targetIndex + 1;
}
// Użyj nowej, centralnej funkcji do przesuwania warstw
this.canvas.canvasLayers.moveLayers(this.draggedElements, { toIndex: insertIndex });
log.info(`Dropped ${this.draggedElements.length} layers at position ${insertIndex}`);
}
/**
* Kończy przeciąganie
*/
handleDragEnd(e) {
this.removeDragInsertionLine();
// Usuń klasę dragging ze wszystkich elementów
this.layersContainer.querySelectorAll('.layer-row').forEach(row => {
row.classList.remove('dragging');
});
this.draggedElements = [];
}
/**
* Aktualizuje panel gdy zmienią się warstwy
*/
onLayersChanged() {
this.renderLayers();
}
/**
* Aktualizuje wygląd zaznaczenia w panelu bez pełnego renderowania.
*/
updateSelectionAppearance() {
const sortedLayers = [...this.canvas.layers].sort((a, b) => b.zIndex - a.zIndex);
const layerRows = this.layersContainer.querySelectorAll('.layer-row');
layerRows.forEach((row, index) => {
const layer = sortedLayers[index];
if (this.canvas.selectedLayers.includes(layer)) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
});
}
/**
* Aktualizuje panel gdy zmienią się warstwy (np. dodanie, usunięcie, zmiana kolejności)
* To jest jedyne miejsce, gdzie powinniśmy w pełni renderować panel.
*/
onLayersChanged() {
this.renderLayers();
}
/**
* Aktualizuje panel gdy zmieni się zaznaczenie (wywoływane z zewnątrz).
* Zamiast pełnego renderowania, tylko aktualizujemy wygląd.
*/
onSelectionChanged() {
this.updateSelectionAppearance();
}
/**
* Niszczy panel i czyści event listenery
*/
destroy() {
if (this.container && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
this.container = null;
this.layersContainer = null;
this.draggedElements = [];
this.removeDragInsertionLine();
log.info('CanvasLayersPanel destroyed');
}
}