aboutsummaryrefslogtreecommitdiff
path: root/extensions-builtin
diff options
context:
space:
mode:
Diffstat (limited to 'extensions-builtin')
-rw-r--r--extensions-builtin/Lora/ui_extra_networks_lora.py4
-rw-r--r--extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js328
-rw-r--r--extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py10
-rw-r--r--extensions-builtin/canvas-zoom-and-pan/style.css63
4 files changed, 351 insertions, 54 deletions
diff --git a/extensions-builtin/Lora/ui_extra_networks_lora.py b/extensions-builtin/Lora/ui_extra_networks_lora.py
index 259e99ac..da49790b 100644
--- a/extensions-builtin/Lora/ui_extra_networks_lora.py
+++ b/extensions-builtin/Lora/ui_extra_networks_lora.py
@@ -13,7 +13,7 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
lora.list_available_loras()
def list_items(self):
- for name, lora_on_disk in lora.available_loras.items():
+ for index, (name, lora_on_disk) in enumerate(lora.available_loras.items()):
path, ext = os.path.splitext(lora_on_disk.filename)
alias = lora_on_disk.get_alias()
@@ -27,6 +27,8 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
"prompt": json.dumps(f"<lora:{alias}:") + " + opts.extra_networks_default_multiplier + " + json.dumps(">"),
"local_preview": f"{path}.{shared.opts.samples_format}",
"metadata": json.dumps(lora_on_disk.metadata, indent=4) if lora_on_disk.metadata else None,
+ "sort_keys": {'default': index, **self.get_sort_keys(lora_on_disk.filename)},
+
}
def allowed_directories_for_previews(self):
diff --git a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
index f555960d..2a2ed999 100644
--- a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
+++ b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
@@ -1,5 +1,3 @@
-// Main
-
// Helper functions
// Get active tab
function getActiveTab(elements, all = false) {
@@ -14,27 +12,142 @@ function getActiveTab(elements, all = false) {
}
}
+// Get tab ID
+function getTabId(elements, elementIDs) {
+ const activeTab = getActiveTab(elements);
+ const tabIdLookup = {
+ "Sketch": elementIDs.sketch,
+ "Inpaint sketch": elementIDs.inpaintSketch,
+ "Inpaint": elementIDs.inpaint
+ };
+ return tabIdLookup[activeTab.innerText];
+}
+
+// Wait until opts loaded
+async function waitForOpts() {
+ return new Promise(resolve => {
+ const checkInterval = setInterval(() => {
+ if (window.opts && Object.keys(window.opts).length !== 0) {
+ clearInterval(checkInterval);
+ resolve(window.opts);
+ }
+ }, 100);
+ });
+}
+
+// Check is hotkey valid
+function isSingleLetter(value) {
+ return (
+ typeof value === "string" && value.length === 1 && /[a-z]/i.test(value)
+ );
+}
+
+// Create hotkeyConfig from opts
+function createHotkeyConfig(defaultHotkeysConfig, hotkeysConfigOpts) {
+ const result = {};
+ const usedKeys = new Set();
+
+ for (const key in defaultHotkeysConfig) {
+ if (typeof hotkeysConfigOpts[key] === "boolean") {
+ result[key] = hotkeysConfigOpts[key];
+ continue;
+ }
+ if (
+ hotkeysConfigOpts[key] &&
+ isSingleLetter(hotkeysConfigOpts[key]) &&
+ !usedKeys.has(hotkeysConfigOpts[key].toUpperCase())
+ ) {
+ // If the property passed the test and has not yet been used, add 'Key' before it and save it
+ result[key] = "Key" + hotkeysConfigOpts[key].toUpperCase();
+ usedKeys.add(hotkeysConfigOpts[key].toUpperCase());
+ } else {
+ // If the property does not pass the test or has already been used, we keep the default value
+ console.error(
+ `Hotkey: ${hotkeysConfigOpts[key]} for ${key} is repeated and conflicts with another hotkey or is not 1 letter. The default hotkey is used: ${defaultHotkeysConfig[key][3]}`
+ );
+ result[key] = defaultHotkeysConfig[key];
+ }
+ }
+
+ return result;
+}
+
+/**
+ * The restoreImgRedMask function displays a red mask around an image to indicate the aspect ratio.
+ * If the image display property is set to 'none', the mask breaks. To fix this, the function
+ * temporarily sets the display property to 'block' and then hides the mask again after 300 milliseconds
+ * to avoid breaking the canvas. Additionally, the function adjusts the mask to work correctly on
+ * very long images.
+ */
+
+function restoreImgRedMask(elements, elementIDs) {
+ const mainTabId = getTabId(elements, elementIDs);
+
+ if (!mainTabId) return;
+
+ const mainTab = gradioApp().querySelector(mainTabId);
+ const img = mainTab.querySelector("img");
+ const imageARPreview = gradioApp().querySelector("#imageARPreview");
+
+ if (!img || !imageARPreview) return;
+
+ imageARPreview.style.transform = "";
+ if (parseFloat(mainTab.style.width) > 865) {
+ const transformString = mainTab.style.transform;
+ const scaleMatch = transformString.match(/scale\(([-+]?[0-9]*\.?[0-9]+)\)/);
+ let zoom = 1; // default zoom
+
+ if (scaleMatch && scaleMatch[1]) {
+ zoom = Number(scaleMatch[1]);
+ }
+
+ imageARPreview.style.transformOrigin = "0 0";
+ imageARPreview.style.transform = `scale(${zoom})`;
+ }
+
+ if (img.style.display !== "none") return;
+
+ img.style.display = "block";
+
+ setTimeout(() => {
+ img.style.display = "none";
+ }, 400);
+}
+
+// Main
onUiLoaded(async() => {
- const hotkeysConfig = {
- resetZoom: "KeyR",
- fitToScreen: "KeyS",
- moveKey: "KeyF",
- overlap: "KeyO"
+ const hotkeysConfigOpts = await waitForOpts();
+
+ // Default config
+ const defaultHotkeysConfig = {
+ canvas_hotkey_reset: "KeyR",
+ canvas_hotkey_fullscreen: "KeyS",
+ canvas_hotkey_move: "KeyF",
+ canvas_hotkey_overlap: "KeyO",
+ canvas_show_tooltip: true,
+ canvas_swap_controls: false
};
+ // swap the actions for ctr + wheel and shift + wheel
+ const hotkeysConfig = createHotkeyConfig(
+ defaultHotkeysConfig,
+ hotkeysConfigOpts
+ );
let isMoving = false;
let mouseX, mouseY;
+ let activeElement;
const elementIDs = {
sketch: "#img2img_sketch",
inpaint: "#img2maskimg",
inpaintSketch: "#inpaint_sketch",
- img2imgTabs: "#mode_img2img .tab-nav"
+ img2imgTabs: "#mode_img2img .tab-nav",
+ rangeGroup: "#img2img_column_size"
};
async function getElements() {
const elements = await Promise.all(
- Object.values(elementIDs).map(id => document.querySelector(id))
+ Object.values(elementIDs).map(id => gradioApp().querySelector(id))
);
return Object.fromEntries(
Object.keys(elementIDs).map((key, index) => [key, elements[index]])
@@ -42,12 +155,94 @@ onUiLoaded(async() => {
}
const elements = await getElements();
+ const elemData = {};
+
+ // Apply functionality to the range inputs. Restore redmask and correct for long images.
+ const rangeInputs = elements.rangeGroup ? elements.rangeGroup.querySelectorAll("input") :
+ [
+ gradioApp().querySelector("#img2img_width input[type='range']"),
+ gradioApp().querySelector("#img2img_height input[type='range']")
+ ];
+
+ rangeInputs.forEach(input => {
+ if (input) {
+ input.addEventListener("input", () => restoreImgRedMask(elements, elementIDs));
+ }
+ });
+
+ function applyZoomAndPan(elemId) {
+ const targetElement = gradioApp().querySelector(elemId);
+
+ if (!targetElement) {
+ console.log("Element not found");
+ return;
+ }
- function applyZoomAndPan(targetElement, elemId) {
targetElement.style.transformOrigin = "0 0";
- let [zoomLevel, panX, panY] = [1, 0, 0];
+
+ elemData[elemId] = {
+ zoom: 1,
+ panX: 0,
+ panY: 0
+ };
let fullScreenMode = false;
+ // Create tooltip
+ function createTooltip() {
+ const toolTipElemnt =
+ targetElement.querySelector(".image-container");
+ const tooltip = document.createElement("div");
+ tooltip.className = "tooltip";
+
+ // Creating an item of information
+ const info = document.createElement("i");
+ info.className = "tooltip-info";
+ info.textContent = "";
+
+ // Create a container for the contents of the tooltip
+ const tooltipContent = document.createElement("div");
+ tooltipContent.className = "tooltip-content";
+
+ // Add info about hotkeys
+ const zoomKey = hotkeysConfig.canvas_swap_controls ? "Ctrl" : "Shift";
+ const adjustKey = hotkeysConfig.canvas_swap_controls ? "Shift" : "Ctrl";
+
+ const hotkeys = [
+ {key: `${zoomKey} + wheel`, action: "Zoom canvas"},
+ {key: `${adjustKey} + wheel`, action: "Adjust brush size"},
+ {
+ key: hotkeysConfig.canvas_hotkey_reset.charAt(hotkeysConfig.canvas_hotkey_reset.length - 1),
+ action: "Reset zoom"
+ },
+ {
+ key: hotkeysConfig.canvas_hotkey_fullscreen.charAt(hotkeysConfig.canvas_hotkey_fullscreen.length - 1),
+ action: "Fullscreen mode"
+ },
+ {
+ key: hotkeysConfig.canvas_hotkey_move.charAt(hotkeysConfig.canvas_hotkey_move.length - 1),
+ action: "Move canvas"
+ }
+ ];
+ hotkeys.forEach(function(hotkey) {
+ const p = document.createElement("p");
+ p.innerHTML =
+ "<b>" + hotkey.key + "</b>" + " - " + hotkey.action;
+ tooltipContent.appendChild(p);
+ });
+
+ // Add information and content elements to the tooltip element
+ tooltip.appendChild(info);
+ tooltip.appendChild(tooltipContent);
+
+ // Add a hint element to the target element
+ toolTipElemnt.appendChild(tooltip);
+ }
+
+ //Show tool tip if setting enable
+ if (hotkeysConfig.canvas_show_tooltip) {
+ createTooltip();
+ }
+
// In the course of research, it was found that the tag img is very harmful when zooming and creates white canvases. This hack allows you to almost never think about this problem, it has no effect on webui.
function fixCanvas() {
const activeTab = getActiveTab(elements).textContent.trim();
@@ -64,12 +259,14 @@ onUiLoaded(async() => {
// Reset the zoom level and pan position of the target element to their initial values
function resetZoom() {
- zoomLevel = 1;
- panX = 0;
- panY = 0;
+ elemData[elemId] = {
+ zoomLevel: 1,
+ panX: 0,
+ panY: 0
+ };
fixCanvas();
- targetElement.style.transform = `scale(${zoomLevel}) translate(${panX}px, ${panY}px)`;
+ targetElement.style.transform = `scale(${elemData[elemId].zoomLevel}) translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px)`;
const canvas = gradioApp().querySelector(
`${elemId} canvas[key="interface"]`
@@ -147,11 +344,14 @@ onUiLoaded(async() => {
// Update the zoom level and pan position of the target element based on the values of the zoomLevel, panX and panY variables
function updateZoom(newZoomLevel, mouseX, mouseY) {
newZoomLevel = Math.max(0.5, Math.min(newZoomLevel, 15));
- panX += mouseX - (mouseX * newZoomLevel) / zoomLevel;
- panY += mouseY - (mouseY * newZoomLevel) / zoomLevel;
+
+ elemData[elemId].panX +=
+ mouseX - (mouseX * newZoomLevel) / elemData[elemId].zoomLevel;
+ elemData[elemId].panY +=
+ mouseY - (mouseY * newZoomLevel) / elemData[elemId].zoomLevel;
targetElement.style.transformOrigin = "0 0";
- targetElement.style.transform = `translate(${panX}px, ${panY}px) scale(${newZoomLevel})`;
+ targetElement.style.transform = `translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px) scale(${newZoomLevel})`;
toggleOverlap("on");
return newZoomLevel;
@@ -159,14 +359,17 @@ onUiLoaded(async() => {
// Change the zoom level based on user interaction
function changeZoomLevel(operation, e) {
- if (e.shiftKey) {
+ if (
+ (!hotkeysConfig.canvas_swap_controls && e.shiftKey) ||
+ (hotkeysConfig.canvas_swap_controls && e.ctrlKey)
+ ) {
e.preventDefault();
let zoomPosX, zoomPosY;
let delta = 0.2;
- if (zoomLevel > 7) {
+ if (elemData[elemId].zoomLevel > 7) {
delta = 0.9;
- } else if (zoomLevel > 2) {
+ } else if (elemData[elemId].zoomLevel > 2) {
delta = 0.6;
}
@@ -174,8 +377,9 @@ onUiLoaded(async() => {
zoomPosY = e.clientY;
fullScreenMode = false;
- zoomLevel = updateZoom(
- zoomLevel + (operation === "+" ? delta : -delta),
+ elemData[elemId].zoomLevel = updateZoom(
+ elemData[elemId].zoomLevel +
+ (operation === "+" ? delta : -delta),
zoomPosX - targetElement.getBoundingClientRect().left,
zoomPosY - targetElement.getBoundingClientRect().top
);
@@ -226,9 +430,9 @@ onUiLoaded(async() => {
targetElement.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
// Update global variables
- zoomLevel = scale;
- panX = offsetX;
- panY = offsetY;
+ elemData[elemId].zoomLevel = scale;
+ elemData[elemId].panX = offsetX;
+ elemData[elemId].panY = offsetY;
fullScreenMode = false;
toggleOverlap("off");
@@ -262,7 +466,8 @@ onUiLoaded(async() => {
targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`;
// Get scrollbar width to right-align the image
- const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
+ const scrollbarWidth =
+ window.innerWidth - document.documentElement.clientWidth;
// Get element and screen dimensions
const elementWidth = targetElement.offsetWidth;
@@ -301,9 +506,9 @@ onUiLoaded(async() => {
targetElement.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
// Update global variables
- zoomLevel = scale;
- panX = offsetX;
- panY = offsetY;
+ elemData[elemId].zoomLevel = scale;
+ elemData[elemId].panX = offsetX;
+ elemData[elemId].panY = offsetY;
fullScreenMode = true;
toggleOverlap("on");
@@ -312,10 +517,9 @@ onUiLoaded(async() => {
// Handle keydown events
function handleKeyDown(event) {
const hotkeyActions = {
- [hotkeysConfig.resetZoom]: resetZoom,
- [hotkeysConfig.overlap]: toggleOverlap,
- [hotkeysConfig.fitToScreen]: fitToScreen
- // [hotkeysConfig.moveKey] : moveCanvas,
+ [hotkeysConfig.canvas_hotkey_reset]: resetZoom,
+ [hotkeysConfig.canvas_hotkey_overlap]: toggleOverlap,
+ [hotkeysConfig.canvas_hotkey_fullscreen]: fitToScreen
};
const action = hotkeyActions[event.code];
@@ -340,6 +544,8 @@ onUiLoaded(async() => {
if (!isKeyDownHandlerAttached) {
document.addEventListener("keydown", handleKeyDown);
isKeyDownHandlerAttached = true;
+
+ activeElement = elemId;
}
}
@@ -347,6 +553,8 @@ onUiLoaded(async() => {
if (isKeyDownHandlerAttached) {
document.removeEventListener("keydown", handleKeyDown);
isKeyDownHandlerAttached = false;
+
+ activeElement = null;
}
}
@@ -369,7 +577,11 @@ onUiLoaded(async() => {
changeZoomLevel(operation, e);
// Handle brush size adjustment with ctrl key pressed
- if (e.ctrlKey || e.metaKey) {
+ if (
+ (hotkeysConfig.canvas_swap_controls && e.shiftKey) ||
+ (!hotkeysConfig.canvas_swap_controls &&
+ (e.ctrlKey || e.metaKey))
+ ) {
e.preventDefault();
// Increase or decrease brush size based on scroll direction
@@ -377,20 +589,19 @@ onUiLoaded(async() => {
}
});
- /**
- * Handle the move event for pan functionality. Updates the panX and panY variables and applies the new transform to the target element.
- * @param {MouseEvent} e - The mouse event.
- */
+ // Handle the move event for pan functionality. Updates the panX and panY variables and applies the new transform to the target element.
function handleMoveKeyDown(e) {
- if (e.code === hotkeysConfig.moveKey) {
- if (!e.ctrlKey && !e.metaKey) {
+ if (e.code === hotkeysConfig.canvas_hotkey_move) {
+ if (!e.ctrlKey && !e.metaKey && isKeyDownHandlerAttached) {
+ e.preventDefault();
+ document.activeElement.blur();
isMoving = true;
}
}
}
function handleMoveKeyUp(e) {
- if (e.code === hotkeysConfig.moveKey) {
+ if (e.code === hotkeysConfig.canvas_hotkey_move) {
isMoving = false;
}
}
@@ -400,21 +611,24 @@ onUiLoaded(async() => {
// Detect zoom level and update the pan speed.
function updatePanPosition(movementX, movementY) {
- let panSpeed = 1.5;
+ let panSpeed = 2;
- if (zoomLevel > 8) {
- panSpeed = 2.5;
+ if (elemData[elemId].zoomLevel > 8) {
+ panSpeed = 3.5;
}
- panX = panX + movementX * panSpeed;
- panY = panY + movementY * panSpeed;
+ elemData[elemId].panX += movementX * panSpeed;
+ elemData[elemId].panY += movementY * panSpeed;
- targetElement.style.transform = `translate(${panX}px, ${panY}px) scale(${zoomLevel})`;
- toggleOverlap("on");
+ // Delayed redraw of an element
+ requestAnimationFrame(() => {
+ targetElement.style.transform = `translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px) scale(${elemData[elemId].zoomLevel})`;
+ toggleOverlap("on");
+ });
}
function handleMoveByKey(e) {
- if (isMoving) {
+ if (isMoving && elemId === activeElement) {
updatePanPosition(e.movementX, e.movementY);
targetElement.style.pointerEvents = "none";
} else {
@@ -422,10 +636,18 @@ onUiLoaded(async() => {
}
}
+ // Prevents sticking to the mouse
+ window.onblur = function() {
+ isMoving = false;
+ };
+
gradioApp().addEventListener("mousemove", handleMoveByKey);
}
- applyZoomAndPan(elements.sketch, elementIDs.sketch);
- applyZoomAndPan(elements.inpaint, elementIDs.inpaint);
- applyZoomAndPan(elements.inpaintSketch, elementIDs.inpaintSketch);
+ applyZoomAndPan(elementIDs.sketch);
+ applyZoomAndPan(elementIDs.inpaint);
+ applyZoomAndPan(elementIDs.inpaintSketch);
+
+ // Make the function global so that other extensions can take advantage of this solution
+ window.applyZoomAndPan = applyZoomAndPan;
});
diff --git a/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py b/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py
new file mode 100644
index 00000000..d83e14da
--- /dev/null
+++ b/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py
@@ -0,0 +1,10 @@
+from modules import shared
+
+shared.options_templates.update(shared.options_section(('canvas_hotkey', "Canvas Hotkeys"), {
+ "canvas_hotkey_move": shared.OptionInfo("F", "Moving the canvas"),
+ "canvas_hotkey_fullscreen": shared.OptionInfo("S", "Fullscreen Mode, maximizes the picture so that it fits into the screen and stretches it to its full width "),
+ "canvas_hotkey_reset": shared.OptionInfo("R", "Reset zoom and canvas positon"),
+ "canvas_hotkey_overlap": shared.OptionInfo("O", "Toggle overlap ( Technical button, neededs for testing )"),
+ "canvas_show_tooltip": shared.OptionInfo(True, "Enable tooltip on the canvas"),
+ "canvas_swap_controls": shared.OptionInfo(False, "Swap hotkey combinations for Zoom and Adjust brush resize"),
+}))
diff --git a/extensions-builtin/canvas-zoom-and-pan/style.css b/extensions-builtin/canvas-zoom-and-pan/style.css
new file mode 100644
index 00000000..5b131d50
--- /dev/null
+++ b/extensions-builtin/canvas-zoom-and-pan/style.css
@@ -0,0 +1,63 @@
+.tooltip-info {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ cursor: help;
+ background-color: rgba(0, 0, 0, 0.3);
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+
+ z-index: 100;
+}
+
+.tooltip-info::after {
+ content: '';
+ display: block;
+ width: 2px;
+ height: 7px;
+ background-color: white;
+ margin-top: 2px;
+}
+
+.tooltip-info::before {
+ content: '';
+ display: block;
+ width: 2px;
+ height: 2px;
+ background-color: white;
+}
+
+.tooltip-content {
+ display: none;
+ background-color: #f9f9f9;
+ color: #333;
+ border: 1px solid #ddd;
+ padding: 15px;
+ position: absolute;
+ top: 40px;
+ left: 10px;
+ width: 250px;
+ font-size: 16px;
+ opacity: 0;
+ border-radius: 8px;
+ box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
+
+ z-index: 100;
+}
+
+.tooltip:hover .tooltip-content {
+ display: block;
+ animation: fadeIn 0.5s;
+ opacity: 1;
+}
+
+@keyframes fadeIn {
+ from {opacity: 0;}
+ to {opacity: 1;}
+}
+