modernized the "Custom Shape Menu"

I updated src/css/custom_shape_menu.css with new styles for a modern look and feel, and I refactored src/CustomShapeMenu.ts to use a new HTML structure for the checkboxes that works with the updated CSS. The menu should now be fully functional with the new design.
This commit is contained in:
Dariusz L
2025-07-28 17:05:57 +02:00
parent d2b7b396aa
commit 7f39cfc8ed
4 changed files with 323 additions and 209 deletions

View File

@@ -86,13 +86,11 @@ export class CustomShapeMenu {
// Add main auto-apply checkbox to the new container
const checkboxContainer = this._createCheckbox(
() => `${this.canvas.autoApplyShapeMask ? "☑" : "☐"} Auto-apply shape mask`,
() => {
// Always hide any active shape preview lines first to prevent them from getting stuck
this.canvas.maskTool.hideShapePreview();
this.canvas.autoApplyShapeMask = !this.canvas.autoApplyShapeMask;
'auto-apply-checkbox',
() => this.canvas.autoApplyShapeMask,
'Auto-apply shape mask',
(e) => {
this.canvas.autoApplyShapeMask = (e.target as HTMLInputElement).checked;
if (this.canvas.autoApplyShapeMask) {
this.canvas.maskTool.applyShapeMask();
log.info("Auto-apply shape mask enabled - mask applied automatically");
@@ -102,7 +100,6 @@ export class CustomShapeMenu {
this.canvas.shapeMaskFeather = false;
log.info("Auto-apply shape mask disabled - mask area removed and sub-options reset.");
}
this._updateUI();
this.canvas.render();
},
@@ -112,11 +109,12 @@ export class CustomShapeMenu {
// Add expansion checkbox
const expansionContainer = this._createCheckbox(
() => `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Expand/Contract mask`,
() => {
this.canvas.shapeMaskExpansion = !this.canvas.shapeMaskExpansion;
'expansion-checkbox',
() => this.canvas.shapeMaskExpansion,
'Expand/Contract mask',
(e) => {
this.canvas.shapeMaskExpansion = (e.target as HTMLInputElement).checked;
this._updateUI();
if (this.canvas.autoApplyShapeMask) {
this.canvas.maskTool.hideShapePreview();
this.canvas.maskTool.applyShapeMask();
@@ -125,7 +123,6 @@ export class CustomShapeMenu {
},
"Dilate (expand) or erode (contract) the shape mask. Positive values expand the mask outward, negative values shrink it inward."
);
expansionContainer.id = 'expansion-checkbox';
featureContainer.appendChild(expansionContainer);
// Add expansion slider container
@@ -205,11 +202,12 @@ export class CustomShapeMenu {
// Add feather checkbox
const featherContainer = this._createCheckbox(
() => `${this.canvas.shapeMaskFeather ? "☑" : "☐"} Feather edges`,
() => {
this.canvas.shapeMaskFeather = !this.canvas.shapeMaskFeather;
'feather-checkbox',
() => this.canvas.shapeMaskFeather,
'Feather edges',
(e) => {
this.canvas.shapeMaskFeather = (e.target as HTMLInputElement).checked;
this._updateUI();
if (this.canvas.autoApplyShapeMask) {
this.canvas.maskTool.hideShapePreview();
this.canvas.maskTool.applyShapeMask();
@@ -218,7 +216,6 @@ export class CustomShapeMenu {
},
"Softens the edges of the shape mask by creating a gradual transition from opaque to transparent."
);
featherContainer.id = 'feather-checkbox';
featureContainer.appendChild(featherContainer);
// Add feather slider container
@@ -288,32 +285,21 @@ export class CustomShapeMenu {
// Add main extension checkbox
const extensionCheckboxContainer = this._createCheckbox(
() => `${this.canvas.outputAreaExtensionEnabled ? "☑" : "☐"} Extend output area`,
() => {
this.canvas.outputAreaExtensionEnabled = !this.canvas.outputAreaExtensionEnabled;
if (this.canvas.outputAreaExtensionEnabled) {
// When enabling, capture current canvas size as the baseline
this.canvas.originalCanvasSize = {
width: this.canvas.width,
height: this.canvas.height
};
// Restore last saved extensions instead of starting from zero
'extension-checkbox',
() => this.canvas.outputAreaExtensionEnabled,
'Extend output area',
(e) => {
this.canvas.outputAreaExtensionEnabled = (e.target as HTMLInputElement).checked;
if (this.canvas.outputAreaExtensionEnabled) {
this.canvas.originalCanvasSize = { width: this.canvas.width, height: this.canvas.height };
this.canvas.outputAreaExtensions = { ...this.canvas.lastOutputAreaExtensions };
log.info(`Captured current canvas size as baseline: ${this.canvas.width}x${this.canvas.height}`);
log.info(`Restored last extensions:`, this.canvas.outputAreaExtensions);
} else {
// Save current extensions before disabling
this.canvas.lastOutputAreaExtensions = { ...this.canvas.outputAreaExtensions };
// Reset current extensions when disabled (but keep the saved ones)
this.canvas.outputAreaExtensions = { top: 0, bottom: 0, left: 0, right: 0 };
log.info(`Saved extensions for later:`, this.canvas.lastOutputAreaExtensions);
}
this._updateExtensionUI();
this._updateCanvasSize(); // Update canvas size when toggling
this._updateCanvasSize();
this.canvas.render();
log.info(`Output area extension ${this.canvas.outputAreaExtensionEnabled ? 'enabled' : 'disabled'}`);
},
"Allows extending the output area boundaries in all directions without changing the custom shape."
);
@@ -424,31 +410,41 @@ export class CustomShapeMenu {
this._addViewportChangeListener();
}
private _createCheckbox(textFn: () => string, clickHandler: () => void, tooltipText?: string): HTMLDivElement {
const container = document.createElement('div');
private _createCheckbox(
id: string,
getChecked: () => boolean,
text: string,
clickHandler: (e: Event) => void,
tooltipText?: string
): HTMLLabelElement {
const container = document.createElement('label');
container.className = 'checkbox-container';
container.htmlFor = id;
container.onmouseover = () => {
container.style.backgroundColor = '#555';
const input = document.createElement('input');
input.type = 'checkbox';
input.id = id;
input.checked = getChecked();
const customCheckbox = document.createElement('div');
customCheckbox.className = 'custom-checkbox';
const labelText = document.createElement('span');
labelText.textContent = text;
container.appendChild(input);
container.appendChild(customCheckbox);
container.appendChild(labelText);
// Stop propagation to prevent menu from closing, but allow default checkbox behavior
container.onclick = (e: MouseEvent) => {
e.stopPropagation();
};
container.onmouseout = () => {
container.style.backgroundColor = 'transparent';
input.onchange = (e: Event) => {
clickHandler(e);
};
const updateText = () => {
container.textContent = textFn();
};
updateText();
container.onclick = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
clickHandler();
updateText();
};
// Add tooltip if provided
if (tooltipText) {
this._addTooltip(container, tooltipText);
}
@@ -458,19 +454,27 @@ export class CustomShapeMenu {
private _updateUI(): void {
if (!this.element) return;
const setChecked = (id: string, checked: boolean) => {
const input = this.element!.querySelector(`#${id}`) as HTMLInputElement;
if (input) input.checked = checked;
};
// Toggle visibility of sub-options based on the main checkbox state
const expansionCheckbox = this.element.querySelector('#expansion-checkbox') as HTMLElement;
setChecked('auto-apply-checkbox', this.canvas.autoApplyShapeMask);
setChecked('expansion-checkbox', this.canvas.shapeMaskExpansion);
setChecked('feather-checkbox', this.canvas.shapeMaskFeather);
setChecked('extension-checkbox', this.canvas.outputAreaExtensionEnabled);
const expansionCheckbox = this.element.querySelector('#expansion-checkbox')?.parentElement as HTMLElement;
if (expansionCheckbox) {
expansionCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'block' : 'none';
expansionCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'flex' : 'none';
}
const featherCheckbox = this.element.querySelector('#feather-checkbox') as HTMLElement;
const featherCheckbox = this.element.querySelector('#feather-checkbox')?.parentElement as HTMLElement;
if (featherCheckbox) {
featherCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'block' : 'none';
featherCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'flex' : 'none';
}
// Update sliders visibility based on their respective checkboxes
const expansionSliderContainer = this.element.querySelector('#expansion-slider-container') as HTMLElement;
if (expansionSliderContainer) {
expansionSliderContainer.style.display = (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskExpansion) ? 'block' : 'none';
@@ -480,20 +484,6 @@ export class CustomShapeMenu {
if (featherSliderContainer) {
featherSliderContainer.style.display = (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskFeather) ? 'block' : 'none';
}
// Update checkbox texts
const checkboxes = this.element.querySelectorAll('div[style*="cursor: pointer"]');
checkboxes.forEach((checkbox, index) => {
if (index === 0) { // Main checkbox
checkbox.textContent = `${this.canvas.autoApplyShapeMask ? "☑" : "☐"} Auto-apply shape mask`;
} else if (index === 1) { // Expansion checkbox
checkbox.textContent = `${this.canvas.shapeMaskExpansion ? "☑" : "☐"} Dilate/Erode mask`;
} else if (index === 2) { // Feather checkbox
checkbox.textContent = `${this.canvas.shapeMaskFeather ? "☑" : "☐"} Feather edges`;
} else if (index === 3) { // Extension checkbox
checkbox.textContent = `${this.canvas.outputAreaExtensionEnabled ? "☑" : "☐"} Extend output area`;
}
});
}
private _updateExtensionUI(): void {

View File

@@ -2,92 +2,165 @@
position: absolute;
top: 0;
left: 0;
background-color: #333;
color: white;
padding: 8px 15px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
background-color: #2f2f2f;
color: #e0e0e0;
padding: 12px;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
display: none;
flex-direction: column;
gap: 4px;
font-family: sans-serif;
font-size: 12px;
gap: 10px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 13px;
z-index: 1001;
border: 1px solid #555;
border: 1px solid #202020;
user-select: none;
min-width: 200px;
min-width: 220px;
}
#layerforge-custom-shape-menu .menu-line {
margin: 2px 0;
line-height: 18px;
font-weight: 600;
color: #4a90e2;
padding-bottom: 5px;
border-bottom: 1px solid #444;
margin-bottom: 5px;
display: flex;
align-items: center;
gap: 8px;
}
#layerforge-custom-shape-menu .feature-container {
background-color: #282828;
background-color: #3a3a3a;
border-radius: 6px;
margin-top: 6px;
padding: 4px 0;
border: 1px solid #444;
padding: 10px;
border: 1px solid #4a4a4a;
}
#layerforge-custom-shape-menu .slider-container {
margin: 0 8px 6px 8px;
padding: 4px 8px;
margin-top: 10px;
display: none;
}
#layerforge-custom-shape-menu .slider-label {
font-size: 11px;
margin-bottom: 4px;
color: #ccc;
font-size: 12px;
margin-bottom: 6px;
color: #e0e0e0;
}
#layerforge-custom-shape-menu input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 4px;
background: #555;
outline: none;
border-radius: 2px;
outline: none;
padding: 0;
margin: 0;
}
#layerforge-custom-shape-menu input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
background: #e0e0e0;
border-radius: 50%;
cursor: pointer;
border: 2px solid #555;
transition: background 0.2s;
}
#layerforge-custom-shape-menu input[type="range"]::-webkit-slider-thumb:hover {
background: #fff;
}
#layerforge-custom-shape-menu input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
background: #e0e0e0;
border-radius: 50%;
cursor: pointer;
border: 2px solid #555;
}
#layerforge-custom-shape-menu .slider-value-display {
font-size: 10px;
font-size: 11px;
text-align: center;
margin-top: 2px;
color: #aaa;
margin-top: 4px;
color: #bbb;
min-height: 14px;
}
#layerforge-custom-shape-menu .extension-slider-container {
margin: 6px 0;
margin: 10px 0;
}
#layerforge-custom-shape-menu .checkbox-container {
margin: 6px 0 2px 0;
padding: 4px 8px;
display: flex;
align-items: center;
gap: 10px;
padding: 6px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s;
line-height: 18px;
position: relative;
}
#layerforge-custom-shape-menu .checkbox-container:hover {
background-color: #555;
background-color: #4a4a4a;
}
#layerforge-custom-shape-menu .checkbox-container input[type="checkbox"] {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
#layerforge-custom-shape-menu .checkbox-container .custom-checkbox {
height: 16px;
width: 16px;
background-color: #2a2a2a;
border: 1px solid #666;
border-radius: 3px;
transition: all 0.2s;
position: relative;
flex-shrink: 0;
}
#layerforge-custom-shape-menu .checkbox-container input:checked ~ .custom-checkbox {
background-color: #3a76d6;
border-color: #3a76d6;
}
#layerforge-custom-shape-menu .checkbox-container .custom-checkbox::after {
content: "";
position: absolute;
display: none;
left: 5px;
top: 1px;
width: 4px;
height: 9px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
#layerforge-custom-shape-menu .checkbox-container input:checked ~ .custom-checkbox::after {
display: block;
}
.layerforge-tooltip {
position: fixed;
background-color: #1a1a1a;
color: #ffffff;
background-color: #2f2f2f;
color: #e0e0e0;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
font-family: sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.4;
max-width: 250px;
word-wrap: break-word;
box-shadow: 0 4px 12px rgba(0,0,0,0.6);
border: 1px solid #444;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
border: 1px solid #202020;
z-index: 10000;
pointer-events: none;
opacity: 0;