MarkLilly's picture
Add files using upload-large-folder tool
70ceb8b verified
Raw History Blame Contribute Delete
15.1 kB
// DialogManager.js - Manages custom input dialogs for ResolutionMaster
import { createModuleLogger } from "./utils/LoggerUtils.js";
import { app } from "../../scripts/app.js";
const log = createModuleLogger('DialogManager');
export class DialogManager {
constructor(resolutionMasterInstance) {
this.rm = resolutionMasterInstance;
this.customInputDialog = null;
this.customInputOverlay = null;
this.inputDialogActive = false;
}
/**
* Shows a custom value input dialog
* @param {string} valueAreaKey - The control key that was clicked
* @param {Event} e - The mouse event
*/
showCustomValueDialog(valueAreaKey, e) {
if (this.inputDialogActive) return;
log.debug(`Clicked on value area: ${valueAreaKey}`);
// Determine the type and current value based on the control key
let valueType, currentValue, propertyName, minValue = 0.01, integerOnly = false;
if (valueAreaKey === 'scaleValueArea') {
valueType = 'Scale Factor';
currentValue = this.rm.node.properties.upscaleValue;
propertyName = 'upscaleValue';
} else if (valueAreaKey === 'resolutionValueArea') {
valueType = 'Resolution Scale';
currentValue = this.rm.calculateResolutionScale(this.rm.node.properties.targetResolution);
propertyName = 'targetResolution';
} else if (valueAreaKey === 'megapixelsValueArea') {
valueType = 'Megapixels';
currentValue = this.rm.node.properties.targetMegapixels;
propertyName = 'targetMegapixels';
} else if (valueAreaKey === 'snapValueArea') {
valueType = 'Snap Value';
currentValue = this.rm.node.properties.snapValue;
propertyName = 'snapValue';
minValue = 1;
integerOnly = true;
} else if (valueAreaKey === 'widthValueArea') {
valueType = 'Width';
currentValue = this.rm.widthWidget ? this.rm.widthWidget.value : this.rm.node.properties.valueX;
propertyName = 'width';
minValue = 64;
integerOnly = true;
} else if (valueAreaKey === 'heightValueArea') {
valueType = 'Height';
currentValue = this.rm.heightWidget ? this.rm.heightWidget.value : this.rm.node.properties.valueY;
propertyName = 'height';
minValue = 64;
integerOnly = true;
} else if (valueAreaKey === 'batchSizeValueArea') {
valueType = 'Batch Size';
currentValue = this.rm.batchSizeWidget ? this.rm.batchSizeWidget.value : 1;
propertyName = 'batch_size';
minValue = 1;
integerOnly = true;
} else {
log.debug(`Unknown value area key: ${valueAreaKey}`);
return;
}
log.debug(`Opening dialog for ${valueType} with current value: ${currentValue}`);
this.createCustomInputDialog(valueType, currentValue, propertyName, minValue, integerOnly, e);
}
/**
* Creates and displays a custom input dialog
* @param {string} valueType - The type of value being edited
* @param {number} currentValue - The current value
* @param {string} propertyName - The property name to update
* @param {number} minValue - Minimum allowed value
* @param {boolean} integerOnly - Whether to allow only integer values
* @param {Event} e - The mouse event for positioning
*/
createCustomInputDialog(valueType, currentValue, propertyName, minValue, integerOnly, e) {
this.inputDialogActive = true;
log.debug(`Creating dialog for ${valueType}, current: ${currentValue}`);
// Create overlay
const overlay = document.createElement('div');
this.customInputOverlay = overlay;
overlay.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5); z-index: 9999;
`;
overlay.addEventListener('mousedown', () => this.closeCustomInputDialog());
document.body.appendChild(overlay);
// Create dialog container
const dialog = document.createElement('div');
this.customInputDialog = dialog;
dialog.className = 'litegraph-custom-input-dialog';
dialog.addEventListener('mousedown', (e) => e.stopPropagation()); // Prevent clicks inside from closing
dialog.style.cssText = `
position: fixed;
background: linear-gradient(135deg, #2a2a2a 0%, #1e1e1e 100%);
border: 2px solid #555; border-radius: 8px; padding: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.8); z-index: 10000;
font-family: Arial, sans-serif; min-width: 280px;
`;
// Position dialog
const x = e.clientX ? e.clientX + 20 : (window.innerWidth - 280) / 2;
const y = e.clientY ? e.clientY + 20 : (window.innerHeight - 200) / 2;
dialog.style.left = `${Math.max(10, Math.min(x, window.innerWidth - 300))}px`;
dialog.style.top = `${Math.max(10, Math.min(y, window.innerHeight - 200))}px`;
// Create dialog content
const inputStep = integerOnly ? '1' : '0.01';
dialog.innerHTML = `
<div style="color: #fff; font-size: 16px; font-weight: bold; margin-bottom: 15px; text-align: center;">Set Custom ${valueType}</div>
<div style="margin-bottom: 10px;">
<label style="color: #ccc; font-size: 12px; display: block; margin-bottom: 5px;">Current: ${this.formatValueForDisplay(currentValue, valueType)}</label>
<input type="${valueType === 'Scale Factor' ? 'text' : 'number'}" id="customValueInput" value="${currentValue}" step="${inputStep}" min="${minValue}"
style="width: 100%; padding: 8px; border: 1px solid #555; border-radius: 4px; background: #333; color: #fff; font-size: 14px; box-sizing: border-box;">
</div>
<div id="validationMessage" style="color: #f55; font-size: 11px; margin-bottom: 5px; min-height: 15px;"></div>
<div id="infoMessage" style="color: #999; font-size: 11px; margin-bottom: 10px; min-height: 15px; text-align: center;"></div>
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button id="cancelBtn" style="padding: 8px 16px; border: 1px solid #555; border-radius: 4px; background: #444; color: #ccc; cursor: pointer; font-size: 12px;">Cancel</button>
<button id="applyBtn" style="padding: 8px 16px; border: 1px solid #5af; border-radius: 4px; background: #5af; color: #fff; cursor: pointer; font-size: 12px;">Apply</button>
</div>
`;
document.body.appendChild(dialog);
// Get elements
const input = dialog.querySelector('#customValueInput');
const validationMsg = dialog.querySelector('#validationMessage');
const infoMsg = dialog.querySelector('#infoMessage');
const cancelBtn = dialog.querySelector('#cancelBtn');
const applyBtn = dialog.querySelector('#applyBtn');
if (valueType === 'Scale Factor') {
infoMsg.textContent = 'Tip: Use /2 for 0.5x, /4 for 0.25x, etc.';
}
// Block decimal characters for integer-only inputs
if (integerOnly) {
input.addEventListener('keydown', (e) => {
// Allow: backspace, delete, tab, escape, enter, arrows, home, end
const allowedKeys = ['Backspace', 'Delete', 'Tab', 'Escape', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
// Block: decimal point, comma, e, E, +, - (except for navigation)
const blockedChars = ['.', ',', 'e', 'E', '+', '-'];
if (allowedKeys.includes(e.key)) {
return; // Allow these keys
}
// Allow Ctrl/Cmd combinations (copy, paste, select all, etc.)
if (e.ctrlKey || e.metaKey) {
return;
}
// Block decimal and scientific notation characters
if (blockedChars.includes(e.key)) {
e.preventDefault();
return;
}
// Allow only digits 0-9
if (!/^\d$/.test(e.key)) {
e.preventDefault();
}
});
}
// Focus and select input
setTimeout(() => { input.focus(); input.select(); }, 50);
// Real-time validation
const validateInput = () => {
const value = this.parseCustomInputValue(input.value, valueType);
if (isNaN(value) || value < minValue) {
let errorMsg = `Value must be ≥ ${minValue}`;
if (typeof input.value === 'string' && input.value.startsWith('/')) {
const divisor = parseFloat(input.value.substring(1));
if (isNaN(divisor) || divisor === 0) errorMsg = 'Invalid divisor after /';
}
validationMsg.textContent = errorMsg;
applyBtn.disabled = true; applyBtn.style.opacity = '0.5';
return false;
} else if (integerOnly && !Number.isInteger(value)) {
validationMsg.textContent = 'Value must be a whole number';
applyBtn.disabled = true; applyBtn.style.opacity = '0.5';
return false;
} else {
validationMsg.textContent = '';
applyBtn.disabled = false; applyBtn.style.opacity = '1';
return true;
}
};
// Event listeners
input.addEventListener('input', validateInput);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && validateInput()) {
this.applyCustomValue(propertyName, this.parseCustomInputValue(input.value, valueType), valueType);
} else if (e.key === 'Escape') {
this.closeCustomInputDialog();
}
});
cancelBtn.addEventListener('click', () => this.closeCustomInputDialog());
applyBtn.addEventListener('click', () => {
if (validateInput()) {
this.applyCustomValue(propertyName, this.parseCustomInputValue(input.value, valueType), valueType);
}
});
validateInput();
}
/**
* Closes and cleans up the custom input dialog
*/
closeCustomInputDialog() {
if (this.customInputDialog) {
document.body.removeChild(this.customInputDialog);
this.customInputDialog = null;
}
if (this.customInputOverlay) {
document.body.removeChild(this.customInputOverlay);
this.customInputOverlay = null;
}
this.inputDialogActive = false;
}
/**
* Parses custom input value, handling special syntax like /2 for division
* @param {string|number} rawValue - The raw input value
* @param {string} valueType - The type of value being parsed
* @returns {number} The parsed numeric value
*/
parseCustomInputValue(rawValue, valueType) {
if (valueType === 'Scale Factor' && typeof rawValue === 'string' && rawValue.startsWith('/')) {
const divisor = parseFloat(rawValue.substring(1));
if (!isNaN(divisor) && divisor !== 0) {
return 1 / divisor;
}
}
return parseFloat(rawValue);
}
/**
* Applies a custom value to the appropriate property
* @param {string} propertyName - The property to update
* @param {number} value - The value to apply
* @param {string} valueType - The type of value
*/
applyCustomValue(propertyName, value, valueType) {
const props = this.rm.node.properties;
if (propertyName === 'upscaleValue') {
props.upscaleValue = value;
if (props.rescaleMode === 'manual') {
this.rm.updateRescaleValue();
}
} else if (propertyName === 'targetResolution') {
// For resolution, we need to reverse-calculate the target resolution from the scale factor
if (this.rm.validateWidgets()) {
const currentPixels = this.rm.widthWidget.value * this.rm.heightWidget.value;
const targetPixels = currentPixels * (value * value);
const targetP = Math.sqrt(targetPixels / (16/9));
props.targetResolution = Math.round(targetP);
if (props.rescaleMode === 'resolution') {
this.rm.updateRescaleValue();
}
}
} else if (propertyName === 'targetMegapixels') {
props.targetMegapixels = value;
if (props.rescaleMode === 'megapixels') {
this.rm.updateRescaleValue();
}
} else if (propertyName === 'snapValue') {
props.snapValue = Math.round(value);
} else if (propertyName === 'width') {
const newWidth = Math.round(value);
const currentHeight = this.rm.heightWidget ? this.rm.heightWidget.value : props.valueY;
this.rm.setDimensions(newWidth, currentHeight);
} else if (propertyName === 'height') {
const newHeight = Math.round(value);
const currentWidth = this.rm.widthWidget ? this.rm.widthWidget.value : props.valueX;
this.rm.setDimensions(currentWidth, newHeight);
} else if (propertyName === 'batch_size') {
const newBatchSize = Math.max(1, Math.min(4096, Math.round(value)));
props.batch_size = newBatchSize;
if (this.rm.batchSizeWidget) {
this.rm.batchSizeWidget.value = newBatchSize;
}
}
this.closeCustomInputDialog();
app.graph.setDirtyCanvas(true);
log.debug(`Applied custom ${valueType}: ${value}`);
}
/**
* Formats a value for display with appropriate units
* @param {number} value - The value to format
* @param {string} valueType - The type of value
* @returns {string} Formatted value with units
*/
formatValueForDisplay(value, valueType) {
if (valueType === 'Scale Factor') {
return value.toFixed(1) + 'x';
} else if (valueType === 'Resolution Scale') {
return '×' + value.toFixed(2);
} else if (valueType === 'Megapixels') {
return value.toFixed(1) + 'MP';
} else if (valueType === 'Width' || valueType === 'Height') {
return value.toString() + 'px';
} else if (valueType === 'Batch Size') {
return value.toString();
} else {
return value.toString();
}
}
}