diff --git a/extensions-builtin/sd_forge_controlnet/javascript/active_units.js b/extensions-builtin/sd_forge_controlnet/javascript/active_units.js index a2662055..9e3a9e72 100644 --- a/extensions-builtin/sd_forge_controlnet/javascript/active_units.js +++ b/extensions-builtin/sd_forge_controlnet/javascript/active_units.js @@ -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); }); diff --git a/extensions-builtin/sd_forge_controlnet/javascript/openpose_editor.js b/extensions-builtin/sd_forge_controlnet/javascript/openpose_editor.js index 1c7b570a..350b831b 100644 --- a/extensions-builtin/sd_forge_controlnet/javascript/openpose_editor.js +++ b/extensions-builtin/sd_forge_controlnet/javascript/openpose_editor.js @@ -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); diff --git a/extensions-builtin/sd_forge_controlnet/javascript/photopea.js b/extensions-builtin/sd_forge_controlnet/javascript/photopea.js index d2b1ebc9..f765fc3a 100644 --- a/extensions-builtin/sd_forge_controlnet/javascript/photopea.js +++ b/extensions-builtin/sd_forge_controlnet/javascript/photopea.js @@ -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); diff --git a/extensions-builtin/sd_forge_controlnet/lib_controlnet/controlnet_ui/controlnet_ui_group.py b/extensions-builtin/sd_forge_controlnet/lib_controlnet/controlnet_ui/controlnet_ui_group.py index b3e02f4e..59549c9e 100644 --- a/extensions-builtin/sd_forge_controlnet/lib_controlnet/controlnet_ui/controlnet_ui_group.py +++ b/extensions-builtin/sd_forge_controlnet/lib_controlnet/controlnet_ui/controlnet_ui_group.py @@ -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, diff --git a/extensions-builtin/sd_forge_controlnet/scripts/controlnet.py b/extensions-builtin/sd_forge_controlnet/scripts/controlnet.py index fa4ab821..00e671eb 100644 --- a/extensions-builtin/sd_forge_controlnet/scripts/controlnet.py +++ b/extensions-builtin/sd_forge_controlnet/scripts/controlnet.py @@ -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) diff --git a/extensions-builtin/sd_forge_controlnet/style.css b/extensions-builtin/sd_forge_controlnet/style.css index 2e15e8d4..785e9370 100644 --- a/extensions-builtin/sd_forge_controlnet/style.css +++ b/extensions-builtin/sd_forge_controlnet/style.css @@ -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); -} +} \ No newline at end of file