Merge pull request #26 from lllyasviel/acc

Put unit in accordions
This commit is contained in:
Chenlei Hu
2024-02-02 22:10:08 +00:00
committed by GitHub
6 changed files with 88 additions and 103 deletions

View File

@@ -1,5 +1,5 @@
/**
* Give a badge on ControlNet Accordion indicating total number of active
* 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.
@@ -66,30 +66,39 @@
constructor(tab, accordion) {
this.tab = tab;
this.accordion = accordion;
this.isImg2Img = tab.querySelector('.cnet-unit-enabled').id.includes('img2img');
this.isImg2Img = tab.querySelector('.cnet-mask-upload').id.includes('img2img');
this.enabledCheckbox = tab.querySelector('.cnet-unit-enabled input');
this.enabledCheckbox = tab.querySelector('.input-accordion-checkbox');
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');
const tabs = tab.parentNode;
this.tabNav = tabs.querySelector('.tab-nav');
this.tabIndex = childIndex(tab) - 1; // -1 because tab-nav is also at the same level.
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.enabledCheckbox.click();
this.enabledCheckbox.click();
this.attachEnabledButtonListener();
this.attachControlTypeRadioListener();
this.attachTabNavChangeObserver();
this.attachImageUploadListener();
this.attachImageStateChangeObserver();
this.attachA1111SendInfoObserver();
this.attachPresetDropdownObserver();
}
getTabNavButton() {
return this.tabNav.querySelector(`:nth-child(${this.tabIndex + 1})`);
/**
* Get the span that has text "Unit {X}".
*/
getUnitHeaderTextElement() {
return this.tab.querySelector(
`:nth-child(${this.tabIndex + 1}) span.svelte-s1r2yt`
);
}
getActiveControlType() {
@@ -102,13 +111,13 @@
}
updateActiveState() {
const tabNavButton = this.getTabNavButton();
if (!tabNavButton) return;
const unitHeader = this.getUnitHeaderTextElement();
if (!unitHeader) return;
if (this.enabledCheckbox.checked) {
tabNavButton.classList.add('cnet-unit-active');
unitHeader.classList.add('cnet-unit-active');
} else {
tabNavButton.classList.remove('cnet-unit-active');
unitHeader.classList.remove('cnet-unit-active');
}
}
@@ -144,11 +153,11 @@
* Add the active control type to tab displayed text.
*/
updateActiveControlType() {
const tabNavButton = this.getTabNavButton();
if (!tabNavButton) return;
const unitHeader = this.getUnitHeaderTextElement();
if (!unitHeader) return;
// Remove the control if exists
const controlTypeSuffix = tabNavButton.querySelector('.control-type-suffix');
const controlTypeSuffix = unitHeader.querySelector('.control-type-suffix');
if (controlTypeSuffix) controlTypeSuffix.remove();
// Add new suffix.
@@ -158,31 +167,7 @@
const span = document.createElement('span');
span.innerHTML = `[${controlType}]`;
span.classList.add('control-type-suffix');
tabNavButton.appendChild(span);
}
/**
* When 'Inpaint' control type is selected in img2img:
* - Make image input disabled
* - Clear existing image input
*/
updateImageInputState() {
if (!this.isImg2Img) return;
const tabNavButton = this.getTabNavButton();
if (!tabNavButton) return;
const controlType = this.getActiveControlType();
if (controlType.toLowerCase() === 'inpaint') {
this.inputImage.disabled = true;
this.inputImage.parentNode.addEventListener('click', imageInputDisabledAlert);
const removeButton = this.tab.querySelector(
'.cnet-input-image-group .cnet-image button[aria-label="Remove Image"]');
if (removeButton) removeButton.click();
} else {
this.inputImage.disabled = false;
this.inputImage.parentNode.removeEventListener('click', imageInputDisabledAlert);
}
unitHeader.appendChild(span);
}
attachEnabledButtonListener() {
@@ -200,22 +185,6 @@
}
}
/**
* Each time the active tab change, all tab nav buttons are cleared and
* regenerated by gradio. So we need to reapply the active states on
* them.
*/
attachTabNavChangeObserver() {
new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
this.updateActiveState();
this.updateActiveControlType();
}
}
}).observe(this.tabNav, { childList: true });
}
attachImageUploadListener() {
// Automatically check `enable` checkbox when image is uploaded.
this.inputImage.addEventListener('change', (event) => {
@@ -303,7 +272,7 @@
gradioApp().querySelectorAll('#controlnet').forEach(accordion => {
if (cnetAllAccordions.has(accordion)) return;
accordion.querySelectorAll('.cnet-unit-tab')
accordion.querySelectorAll('.input-accordion')
.forEach(tab => new ControlNetUnitTab(tab, accordion));
cnetAllAccordions.add(accordion);
});

View File

@@ -56,7 +56,7 @@
}
});
}
const tabs = gradioApp().querySelectorAll('.cnet-unit-tab');
const tabs = gradioApp().querySelectorAll('#controlnet .input-accordion');
tabs.forEach(tab => {
if (cnetOpenposeEditorRegisteredElements.has(tab)) return;
cnetOpenposeEditorRegisteredElements.add(tab);

View File

@@ -4,12 +4,12 @@
Copyright 2011 Jon Leighton
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
@@ -298,13 +298,13 @@
}
/**
* Fetch detected maps from each ControlNet units.
* Fetch detected maps from each ControlNet units.
* Create a new photopea document.
* Add those detected maps to the created document.
*/
async fetchFromControlNet(tabs) {
if (tabs.length === 0) return;
const isImg2Img = tabs[0].querySelector('.cnet-unit-enabled').id.includes('img2img');
const isImg2Img = tabs[0].querySelector('.cnet-mask-upload').id.includes('img2img');
const generationType = isImg2Img ? 'img2img' : 'txt2img';
const width = gradioApp().querySelector(`#${generationType}_width input[type=number]`).value;
const height = gradioApp().querySelector(`#${generationType}_height input[type=number]`).value;
@@ -401,7 +401,7 @@
}
const closeModalButton = accordion.querySelector('.cnet-photopea-edit .cnet-modal-close');
const tabs = accordion.querySelectorAll('.cnet-unit-tab');
const tabs = accordion.querySelectorAll('.controlnet .input-accordion');
const photopeaIframe = accordion.querySelector('.photopea-iframe');
const photopeaContext = new PhotopeaContext(photopeaIframe, tabs);

View File

@@ -59,7 +59,7 @@ class A1111Context:
)
@property
def img2img_non_inpaint_tabs(self) -> List[gr.components.IOComponent]:
def img2img_non_inpaint_tabs(self) -> Tuple[gr.components.IOComponent]:
return (
self.img2img_img2img_tab,
self.img2img_img2img_sketch_tab,
@@ -151,7 +151,8 @@ class ControlNetUiGroup(object):
self,
is_img2img: bool,
default_unit: external_code.ControlNetUnit,
photopea: Optional[Photopea],
unit_enabled: gr.Checkbox,
photopea: Optional[Photopea] = None,
):
# Whether callbacks have been registered.
self.callbacks_registered: bool = False
@@ -164,6 +165,8 @@ class ControlNetUiGroup(object):
self.webcam_enabled = False
self.webcam_mirrored = False
# Now the enabled checkbox is moved to display on InputAccordion.
self.enabled = unit_enabled
# Note: All gradio elements declared in `render` will be defined as member variable.
# Update counter to trigger a force update of UiControlNetUnit.
# This is useful when a field with no event subscriber available changes.
@@ -190,7 +193,6 @@ class ControlNetUiGroup(object):
self.webcam_enable = None
self.webcam_mirror = None
self.send_dimen_button = None
self.enabled = None
self.pixel_perfect = None
self.preprocessor_preview = None
self.mask_upload = None
@@ -393,18 +395,6 @@ class ControlNetUiGroup(object):
)
with FormRow(elem_classes=["controlnet_main_options"]):
self.enabled = gr.Checkbox(
label="Enable",
value=self.default_unit.enabled,
elem_id=f"{elem_id_tabname}_{tabname}_controlnet_enable_checkbox",
elem_classes=["cnet-unit-enabled"],
)
# self.low_vram = gr.Checkbox(
# label="Low VRAM",
# value=self.default_unit.low_vram,
# elem_id=f"{elem_id_tabname}_{tabname}_controlnet_low_vram_checkbox",
# visible=False, # Not needed now
# )
self.pixel_perfect = gr.Checkbox(
label="Pixel Perfect",
value=self.default_unit.pixel_perfect,

View File

@@ -5,7 +5,8 @@ import cv2
import torch
import modules.scripts as scripts
from modules import shared, script_callbacks, processing, masking, images
from modules import shared, script_callbacks, masking, images
from modules.ui_components import InputAccordion
from modules.api.api import decode_base64_to_image
import gradio as gr
@@ -58,33 +59,32 @@ class ControlNetForForgeOfficial(scripts.Script):
def show(self, is_img2img):
return scripts.AlwaysVisible
def uigroup(self, tabname: str, is_img2img: bool, elem_id_tabname: str, photopea: Optional[Photopea]) -> Tuple[ControlNetUiGroup, gr.State]:
default_unit = UiControlNetUnit(enabled=False, module="None", model="None")
group = ControlNetUiGroup(is_img2img, default_unit, photopea)
return group, group.render(tabname, elem_id_tabname)
def ui(self, is_img2img):
infotext = Infotext()
ui_groups = []
controls = []
max_models = shared.opts.data.get("control_net_unit_count", 3)
elem_id_tabname = ("img2img" if is_img2img else "txt2img") + "_controlnet"
gen_type = "img2img" if is_img2img else "txt2img"
elem_id_tabname = gen_type + "_controlnet"
default_unit = UiControlNetUnit(enabled=False, module="None", model="None")
with gr.Group(elem_id=elem_id_tabname):
with gr.Accordion(f"ControlNet Integrated", open=False, elem_id="controlnet"):
photopea = Photopea() if not shared.opts.data.get("controlnet_disable_photopea_edit", False) else None
if max_models > 1:
with gr.Tabs(elem_id=f"{elem_id_tabname}_tabs"):
for i in range(max_models):
with gr.Tab(f"ControlNet Unit {i}",
elem_classes=['cnet-unit-tab']):
group, state = self.uigroup(f"ControlNet-{i}", is_img2img, elem_id_tabname, photopea)
ui_groups.append(group)
controls.append(state)
else:
with gr.Column():
group, state = self.uigroup(f"ControlNet", is_img2img, elem_id_tabname, photopea)
ui_groups.append(group)
controls.append(state)
with gr.Accordion(f"ControlNet Integrated", open=False, elem_id="controlnet",
elem_classes=["controlnet"]):
photopea = (
Photopea()
if not shared.opts.data.get("controlnet_disable_photopea_edit", False)
else None
)
with gr.Row(elem_id=elem_id_tabname + "_accordions", elem_classes="accordions"):
for i in range(max_models):
with InputAccordion(
value=False,
label=f"ControlNet Unit {i}",
elem_classes=["cnet-unit-enabled"],
) as enable_unit:
group = ControlNetUiGroup(is_img2img, default_unit, enable_unit, photopea)
ui_groups.append(group)
controls.append(group.render(f"ControlNet-{i}", elem_id_tabname))
for i, ui_group in enumerate(ui_groups):
infotext.register_unit(i, ui_group)

View File

@@ -1,3 +1,29 @@
/* InputAccordion alignment */
/* Flex container */
.controlnet .svelte-vt1mxs {
display: flex;
flex-wrap: wrap;
flex-direction: row;
gap: 10px;
/* Adjusts the space between items */
}
.controlnet .input-accordion {
flex: 1 1 calc(50% - 10px);
/* Adjusts for the gap, default 2 columns */
/* Additional styling for items */
}
/* Media query for screens smaller than a specific width */
@media (max-width: 600px) {
/* Adjust the threshold as needed */
.controlnet .input-accordion {
flex: 1 1 100%;
/* Changes to 1 column when window width is ≤ 600px */
}
}
.cnet-modal {
display: none;
/* Hidden by default */
@@ -179,4 +205,4 @@
border-radius: var(--radius-sm);
background: var(--background-fill-primary);
color: var(--block-label-text-color);
}
}