Files
stable-diffusion-webui-forge/extensions-builtin/sd_forge_controlnet/javascript/active_units.js
Chenlei Hu 3e80e11309 Add back enabled checkbox and sync (#30)
* Add back enabled checkbox and sync

* Better state management
2024-02-02 17:57:45 -05:00

297 lines
12 KiB
JavaScript

/**
* Give a badge on ControlNet Accordion indicating total number of active
* units.
* Make active unit's tab name green.
* Append control type to tab name.
* Disable resize mode selection when A1111 img2img input is used.
*/
(function () {
const cnetAllAccordions = new Set();
onUiUpdate(() => {
const ImgChangeType = {
NO_CHANGE: 0,
REMOVE: 1,
ADD: 2,
SRC_CHANGE: 3,
};
function imgChangeObserved(mutationsList) {
// Iterate over all mutations that just occured
for (let mutation of mutationsList) {
// Check if the mutation is an addition or removal of a node
if (mutation.type === 'childList') {
// Check if nodes were added
if (mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.tagName === 'IMG') {
return ImgChangeType.ADD;
}
}
}
// Check if nodes were removed
if (mutation.removedNodes.length > 0) {
for (const node of mutation.removedNodes) {
if (node.tagName === 'IMG') {
return ImgChangeType.REMOVE;
}
}
}
}
// Check if the mutation is a change of an attribute
else if (mutation.type === 'attributes') {
if (mutation.target.tagName === 'IMG' && mutation.attributeName === 'src') {
return ImgChangeType.SRC_CHANGE;
}
}
}
return ImgChangeType.NO_CHANGE;
}
function childIndex(element) {
// Get all child nodes of the parent
let children = Array.from(element.parentNode.childNodes);
// Filter out non-element nodes (like text nodes and comments)
children = children.filter(child => child.nodeType === Node.ELEMENT_NODE);
return children.indexOf(element);
}
function imageInputDisabledAlert() {
alert('Inpaint control type must use a1111 input in img2img mode.');
}
class ControlNetUnitTab {
constructor(tab, accordion) {
this.tab = tab;
this.accordion = accordion;
this.isImg2Img = tab.querySelector('.cnet-mask-upload').id.includes('img2img');
this.enabledAccordionCheckbox = tab.querySelector('.input-accordion-checkbox');
this.enabledCheckbox = tab.querySelector('.cnet-unit-enabled input');
this.inputImage = tab.querySelector('.cnet-input-image-group .cnet-image input[type="file"]');
this.inputImageContainer = tab.querySelector('.cnet-input-image-group .cnet-image');
this.controlTypeRadios = tab.querySelectorAll('.controlnet_control_type_filter_group input[type="radio"]');
this.resizeModeRadios = tab.querySelectorAll('.controlnet_resize_mode_radio input[type="radio"]');
this.runPreprocessorButton = tab.querySelector('.cnet-run-preprocessor');
this.tabs = tab.parentNode;
this.tabIndex = childIndex(tab);
// By default the InputAccordion checkbox is linked with the state
// of accordion's open/close state. To disable this link, we can
// simulate click to check the checkbox and uncheck it.
this.enabledAccordionCheckbox.click();
this.enabledAccordionCheckbox.click();
this.sync_enabled_checkbox();
this.attachEnabledButtonListener();
this.attachControlTypeRadioListener();
this.attachImageUploadListener();
this.attachImageStateChangeObserver();
this.attachA1111SendInfoObserver();
this.attachPresetDropdownObserver();
}
/**
* Sync the states of enabledCheckbox and enabledAccordionCheckbox.
*/
sync_enabled_checkbox() {
this.enabledCheckbox.addEventListener("change", () => {
if (this.enabledAccordionCheckbox.checked != this.enabledCheckbox.checked) {
this.enabledAccordionCheckbox.click();
}
});
this.enabledAccordionCheckbox.addEventListener("change", () => {
if (this.enabledCheckbox.checked != this.enabledAccordionCheckbox.checked) {
this.enabledCheckbox.click();
}
});
}
/**
* Get the span that has text "Unit {X}".
*/
getUnitHeaderTextElement() {
return this.tab.querySelector(
`:nth-child(${this.tabIndex + 1}) span.svelte-s1r2yt`
);
}
getActiveControlType() {
for (let radio of this.controlTypeRadios) {
if (radio.checked) {
return radio.value;
}
}
return undefined;
}
updateActiveState() {
const unitHeader = this.getUnitHeaderTextElement();
if (!unitHeader) return;
if (this.enabledCheckbox.checked) {
unitHeader.classList.add('cnet-unit-active');
} else {
unitHeader.classList.remove('cnet-unit-active');
}
}
updateActiveUnitCount() {
function getActiveUnitCount(checkboxes) {
let activeUnitCount = 0;
for (const checkbox of checkboxes) {
if (checkbox.checked)
activeUnitCount++;
}
return activeUnitCount;
}
const checkboxes = this.accordion.querySelectorAll('.cnet-unit-enabled input');
const span = this.accordion.querySelector('.label-wrap span');
// Remove existing badge.
if (span.childNodes.length !== 1) {
span.removeChild(span.lastChild);
}
// Add new badge if necessary.
const activeUnitCount = getActiveUnitCount(checkboxes);
if (activeUnitCount > 0) {
const div = document.createElement('div');
div.classList.add('cnet-badge');
div.classList.add('primary');
div.innerHTML = `${activeUnitCount} unit${activeUnitCount > 1 ? 's' : ''}`;
span.appendChild(div);
}
}
/**
* Add the active control type to tab displayed text.
*/
updateActiveControlType() {
const unitHeader = this.getUnitHeaderTextElement();
if (!unitHeader) return;
// Remove the control if exists
const controlTypeSuffix = unitHeader.querySelector('.control-type-suffix');
if (controlTypeSuffix) controlTypeSuffix.remove();
// Add new suffix.
const controlType = this.getActiveControlType();
if (controlType === 'All') return;
const span = document.createElement('span');
span.innerHTML = `[${controlType}]`;
span.classList.add('control-type-suffix');
unitHeader.appendChild(span);
}
attachEnabledButtonListener() {
this.enabledCheckbox.addEventListener('change', () => {
this.updateActiveState();
this.updateActiveUnitCount();
});
}
attachControlTypeRadioListener() {
for (const radio of this.controlTypeRadios) {
radio.addEventListener('change', () => {
this.updateActiveControlType();
});
}
}
attachImageUploadListener() {
// Automatically check `enable` checkbox when image is uploaded.
this.inputImage.addEventListener('change', (event) => {
if (!event.target.files) return;
if (!this.enabledCheckbox.checked)
this.enabledCheckbox.click();
});
// Automatically check `enable` checkbox when JSON pose file is uploaded.
this.tab.querySelector('.cnet-upload-pose input').addEventListener('change', (event) => {
if (!event.target.files) return;
if (!this.enabledCheckbox.checked)
this.enabledCheckbox.click();
});
}
attachImageStateChangeObserver() {
new MutationObserver((mutationsList) => {
const changeObserved = imgChangeObserved(mutationsList);
if (changeObserved === ImgChangeType.ADD) {
// enabling the run preprocessor button
this.runPreprocessorButton.removeAttribute("disabled");
this.runPreprocessorButton.title = 'Run preprocessor';
}
if (changeObserved === ImgChangeType.REMOVE) {
// disabling the run preprocessor button
this.runPreprocessorButton.setAttribute("disabled", true);
this.runPreprocessorButton.title = "No ControlNet input image available";
}
}).observe(this.inputImageContainer, {
childList: true,
subtree: true,
});
}
/**
* Observe send PNG info buttons in A1111, as they can also directly
* set states of ControlNetUnit.
*/
attachA1111SendInfoObserver() {
const pasteButtons = gradioApp().querySelectorAll('#paste');
const pngButtons = gradioApp().querySelectorAll(
this.isImg2Img ?
'#img2img_tab, #inpaint_tab' :
'#txt2img_tab'
);
for (const button of [...pasteButtons, ...pngButtons]) {
button.addEventListener('click', () => {
// The paste/send img generation info feature goes
// though gradio, which is pretty slow. Ideally we should
// observe the event when gradio has done the job, but
// that is not an easy task.
// Here we just do a 2 second delay until the refresh.
setTimeout(() => {
this.updateActiveState();
this.updateActiveUnitCount();
}, 2000);
});
}
}
attachPresetDropdownObserver() {
const presetDropDown = this.tab.querySelector('.cnet-preset-dropdown');
new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.removedNodes.length > 0) {
setTimeout(() => {
this.updateActiveState();
this.updateActiveUnitCount();
this.updateActiveControlType();
}, 1000);
return;
}
}
}).observe(presetDropDown, {
childList: true,
subtree: true,
});
}
}
gradioApp().querySelectorAll('#controlnet').forEach(accordion => {
if (cnetAllAccordions.has(accordion)) return;
accordion.querySelectorAll('.input-accordion')
.forEach(tab => new ControlNetUnitTab(tab, accordion));
cnetAllAccordions.add(accordion);
});
});
})();