/** * Dialog UI components for controller mapping * Handles dialog creation, display, and interaction */ import { app } from "../../../../scripts/app.js"; import { createMappingList } from "./mappingComponent.js"; import { cancelLearning, completeMapping } from "../core/learningManager.js"; import { contextProvider } from "../core/contextProvider.js"; import { eventBus } from "../core/eventBus.js"; // Get instances from context instead of globals const getContext = () => { const mappingEngine = contextProvider.get('mappingEngine'); const learningState = contextProvider.get('learningState'); if (!mappingEngine || !learningState) { console.warn("ControlFreak: Core components not available in dialog, functionality may be limited"); } return { mappingEngine, learningState }; }; /** * Show the learning dialog * @param {Object} learningTarget - The target information from learningManager.js * (e.g., { type: 'widget', nodeId: ..., widgetName: ..., nodeTitle: ..., widgetConfig: ... }) * @param {Object} callbacks - Optional callbacks for dialog actions * @returns {Object} Dialog elements */ function showLearningDialog(learningTarget, callbacks = {}) { // First, remove any existing dialog const existingDialog = document.getElementById("controller-learning-dialog"); if (existingDialog) { existingDialog.remove(); } // Create dialog container const dialog = document.createElement("div"); dialog.className = "controller-dialog controller-mapping-dialog"; dialog.id = "controller-learning-dialog"; // Add ID for easier selection // Store dialog in learning state const { learningState } = getContext(); if (learningState) { learningState.setDialog(dialog); } else { console.warn("ControlFreak Dialog: Learning state not available, dialog may not work properly"); } // Create dialog header const header = document.createElement("div"); header.className = "controller-dialog-header"; // Add branded title with icon const title = document.createElement("h2"); title.className = "cf-dialog-title"; title.innerHTML = ` `; header.appendChild(title); const closeButton = document.createElement("button"); closeButton.className = "controller-dialog-close"; closeButton.innerHTML = "×"; closeButton.onclick = () => { if (callbacks.onCancel) { callbacks.onCancel(); } else { cancelLearning(); // Call the client-side cancel function } }; header.appendChild(closeButton); dialog.appendChild(header); // Create dialog content const content = document.createElement("div"); content.className = "controller-dialog-content"; // Add a subtitle const subtitle = document.createElement("div"); subtitle.className = "cf-dialog-subtitle"; subtitle.textContent = "Controller Mapping"; content.appendChild(subtitle); // Target info section const targetSection = document.createElement("div"); targetSection.className = "controller-mapping-target"; const targetTitle = document.createElement("h3"); targetTitle.textContent = "Target"; targetSection.appendChild(targetTitle); const targetInfo = document.createElement("div"); targetInfo.className = "controller-mapping-target-info"; if (learningTarget.type === "widget") { targetInfo.innerHTML = `
Type: Node Widget
Node: ${learningTarget.nodeTitle || learningTarget.nodeId}
Widget: ${learningTarget.widgetName}
`; } else if (learningTarget.type === "ui_element") { targetInfo.innerHTML = `
Type: UI Element
Element ID: ${learningTarget.elementId}
`; } else if (learningTarget.type === "command") { targetInfo.innerHTML = `
Type: Command
Command: ${learningTarget.label || learningTarget.commandId}
`; } else { targetInfo.textContent = "Unknown target type."; } targetSection.appendChild(targetInfo); content.appendChild(targetSection); // Existing mappings section const existingMappingsSection = document.createElement("div"); existingMappingsSection.className = "controller-mapping-existing"; const existingMappingsTitle = document.createElement("h3"); existingMappingsTitle.textContent = "Existing Mappings"; existingMappingsSection.appendChild(existingMappingsTitle); // Create a placeholder for the existing mappings list const mappingsPlaceholder = document.createElement("div"); mappingsPlaceholder.className = "controller-mapping-existing-content"; mappingsPlaceholder.textContent = "Loading existing mappings..."; existingMappingsSection.appendChild(mappingsPlaceholder); // Get existing mappings for this target try { // Get mappingEngine from context const { mappingEngine } = getContext(); if (!mappingEngine) { mappingsPlaceholder.textContent = "MappingEngine not available"; } else { // Get all mappings from current profile const allMappings = mappingEngine.getMappings(); // Filter for mappings targeting this specific widget/command/element const filteredMappings = allMappings.filter(mapping => { if (learningTarget.type === "widget" && mapping.target.type === "widget") { return ( mapping.target.nodeId === learningTarget.nodeId && mapping.target.widgetName === learningTarget.widgetName ); } else if (learningTarget.type === "command" && mapping.target.type === "command") { return mapping.target.commandId === learningTarget.commandId; } else if (learningTarget.type === "ui_element" && mapping.target.type === "ui_element") { return mapping.target.elementId === learningTarget.elementId; } return false; }); // Create the mappings list and update the placeholder try { // Clear the placeholder mappingsPlaceholder.innerHTML = ''; // Create mapping list const mappingsList = createMappingList(filteredMappings); // Add the mappings list to the placeholder mappingsPlaceholder.appendChild(mappingsList); // If no mappings, createMappingList will handle showing a message } catch (error) { console.error("Error creating mappings list:", error); mappingsPlaceholder.textContent = "Error loading mappings."; } } } catch (error) { console.error("Error setting up existing mappings:", error); mappingsPlaceholder.textContent = "Error setting up mappings."; } content.appendChild(existingMappingsSection); // Controller input section const inputSection = document.createElement("div"); inputSection.className = "controller-mapping-input"; const inputTitle = document.createElement("h3"); inputTitle.textContent = "Controller Input"; inputSection.appendChild(inputTitle); // Create the input list container const inputContainer = document.createElement("div"); inputContainer.className = "controller-mapping-input-list"; inputContainer.innerHTML = "
Move a control on your controller to map it
"; inputSection.appendChild(inputContainer); content.appendChild(inputSection); // Mapping type section const mappingTypeSection = document.createElement("div"); mappingTypeSection.className = "controller-mapping-type-section"; const mappingTypeTitle = document.createElement("h3"); mappingTypeTitle.textContent = "Mapping Type"; mappingTypeSection.appendChild(mappingTypeTitle); // Create mapping type selector const mappingTypeContainer = document.createElement("div"); mappingTypeContainer.className = "controller-mapping-type-container"; // Create type selector const typeSelect = document.createElement("select"); typeSelect.className = "controller-mapping-type-select"; // Add mapping type options based on target type if (learningTarget.type === "widget") { // Different mapping types for widgets typeSelect.innerHTML = ` `; } else if (learningTarget.type === "command") { // Command mapping types typeSelect.innerHTML = ` `; } else if (learningTarget.type === "ui_element") { // UI element mapping types typeSelect.innerHTML = ` `; } mappingTypeContainer.appendChild(typeSelect); mappingTypeSection.appendChild(mappingTypeContainer); // Range options for numerical widgets if (learningTarget.type === "widget" && learningTarget.widgetConfig && (learningTarget.widgetConfig.type === "slider" || learningTarget.widgetConfig.type === "number" || learningTarget.widgetConfig.type === "combo")) { // Check for existing mappings with min/max overrides let existingMinMaxOverride = null; try { const { mappingEngine } = getContext(); if (mappingEngine) { const existingMappings = mappingEngine.getMappings().filter(m => m.target && m.target.type === 'widget' && m.target.nodeId === learningTarget.nodeId && m.target.widgetName === learningTarget.widgetName ); // Find first mapping with min/max transform data const existingMapping = existingMappings.find(m => m.transform && (typeof m.transform.targetMin === 'number' || typeof m.transform.targetMax === 'number') ); if (existingMapping && existingMapping.transform) { existingMinMaxOverride = existingMapping.transform; } } } catch (err) { console.warn("Error getting existing mapping transforms:", err); } // Create range inputs for min/max values const rangeContainer = document.createElement("div"); rangeContainer.className = "controller-mapping-range"; // Min value input const minContainer = document.createElement("div"); const minLabel = document.createElement("label"); minLabel.textContent = "Min Value:"; minContainer.appendChild(minLabel); const minInput = document.createElement("input"); minInput.type = "number"; minInput.className = "controller-mapping-min"; // Set default value from existing mapping transform if available, otherwise use widget config if (existingMinMaxOverride && typeof existingMinMaxOverride.targetMin === 'number') { minInput.value = existingMinMaxOverride.targetMin; // Add note that this value is from other mappings const minNote = document.createElement("small"); minNote.className = "controller-mapping-note"; minNote.textContent = "(from existing mapping)"; minContainer.appendChild(minNote); } else if (learningTarget.widgetConfig && learningTarget.widgetConfig.config) { minInput.value = learningTarget.widgetConfig.config.min !== undefined ? learningTarget.widgetConfig.config.min : 0; } else { minInput.value = 0; } minContainer.appendChild(minInput); rangeContainer.appendChild(minContainer); // Max value input const maxContainer = document.createElement("div"); const maxLabel = document.createElement("label"); maxLabel.textContent = "Max Value:"; maxContainer.appendChild(maxLabel); const maxInput = document.createElement("input"); maxInput.type = "number"; maxInput.className = "controller-mapping-max"; // Set default value from existing mapping transform if available, otherwise use widget config if (existingMinMaxOverride && typeof existingMinMaxOverride.targetMax === 'number') { maxInput.value = existingMinMaxOverride.targetMax; // Add note that this value is from other mappings const maxNote = document.createElement("small"); maxNote.className = "controller-mapping-note"; maxNote.textContent = "(from existing mapping)"; maxContainer.appendChild(maxNote); } else if (learningTarget.widgetConfig && learningTarget.widgetConfig.config) { maxInput.value = learningTarget.widgetConfig.config.max !== undefined ? learningTarget.widgetConfig.config.max : 1; } else { maxInput.value = 1; } maxContainer.appendChild(maxInput); rangeContainer.appendChild(maxContainer); // Add step size input for incremental mapping const stepContainer = document.createElement("div"); stepContainer.className = "hidden"; // Hide initially, show only for incremental mapping const stepLabel = document.createElement("label"); stepLabel.textContent = "Step Size:"; stepContainer.appendChild(stepLabel); const stepInput = document.createElement("input"); stepInput.type = "number"; stepInput.className = "controller-mapping-step"; // Set default value from widget config if available if (learningTarget.widgetConfig && learningTarget.widgetConfig.config) { stepInput.value = learningTarget.widgetConfig.config.step !== undefined ? learningTarget.widgetConfig.config.step : 0.1; } else { stepInput.value = 0.1; } stepContainer.appendChild(stepInput); rangeContainer.appendChild(stepContainer); // Show/hide step size based on mapping type typeSelect.addEventListener("change", (e) => { if (e.target.value === "incremental" || e.target.value === "decremental") { stepContainer.classList.remove("hidden"); stepContainer.classList.add("visible"); stepLabel.textContent = "Step Size:"; stepInput.placeholder = ""; } else { stepContainer.classList.add("hidden"); stepContainer.classList.remove("visible"); } }); // Trigger the change event to properly initialize visibility typeSelect.dispatchEvent(new Event("change")); mappingTypeSection.appendChild(rangeContainer); } // Invert Checkbox (always show) const invertContainer = document.createElement("div"); invertContainer.className = "controller-mapping-option"; const invertCheckbox = document.createElement("input"); invertCheckbox.type = "checkbox"; invertCheckbox.id = "controller-mapping-invert"; invertCheckbox.className = "controller-mapping-invert-checkbox"; const invertLabel = document.createElement("label"); invertLabel.htmlFor = "controller-mapping-invert"; invertLabel.textContent = " Invert Output (Map 0-1 to Max-Min)"; invertContainer.appendChild(invertCheckbox); invertContainer.appendChild(invertLabel); mappingTypeSection.appendChild(invertContainer); content.appendChild(mappingTypeSection); // Map button and cancel button const buttonContainer = document.createElement("div"); buttonContainer.className = "controller-mapping-buttons"; const cancelButton = document.createElement("button"); cancelButton.className = "controller-cancel-button"; cancelButton.textContent = "Cancel"; cancelButton.onclick = () => { if (callbacks.onCancel) { callbacks.onCancel(); } else { cancelLearning(); } }; buttonContainer.appendChild(cancelButton); const mapButton = document.createElement("button"); mapButton.className = "controller-map-button"; mapButton.textContent = "Map Control"; mapButton.disabled = true; // Disabled until a control is selected mapButton.onclick = () => { // Get the selected control and mapping options from the learningState const { learningState } = getContext(); // Check for selected control if (!learningState || !learningState.selectedControl) { console.warn("No control selected or learning state unavailable"); showNotification("No control input selected. Please click on a detected control.", "warning"); return; } // Get values from form const mappingType = typeSelect.value; const isInverted = invertCheckbox.checked; // Get min/max values if applicable let targetMin, targetMax, stepSize; const minInput = document.querySelector(".controller-mapping-min"); const maxInput = document.querySelector(".controller-mapping-max"); const stepInput = document.querySelector(".controller-mapping-step"); if (minInput) targetMin = parseFloat(minInput.value); if (maxInput) targetMax = parseFloat(maxInput.value); if (stepInput && (mappingType === "incremental" || mappingType === "decremental")) stepSize = parseFloat(stepInput.value); // Complete the mapping completeMapping( learningState.selectedControl, learningState.learningTarget, targetMin, targetMax, mappingType, stepSize, isInverted ); }; buttonContainer.appendChild(mapButton); content.appendChild(buttonContainer); dialog.appendChild(content); // Add the dialog to the document document.body.appendChild(dialog); return { dialog, inputContainer, mapButton }; } /** * Hide the learning dialog */ function hideLearningDialog() { // Get the learning state manager from context const { learningState } = getContext(); if (learningState) { const dialog = learningState.learningDialog; if (dialog) { // Clean up event listeners if (dialog.cleanupEvents) { dialog.cleanupEvents(); } // Remove the dialog from the DOM if (dialog.parentNode) { dialog.parentNode.removeChild(dialog); } // Clear the dialog reference learningState.setDialog(null); } } else { // Fallback: try to find and remove any dialog with the mapping dialog class const dialogs = document.querySelectorAll('.controller-mapping-dialog'); dialogs.forEach(dialog => { if (dialog.cleanupEvents) { dialog.cleanupEvents(); } if (dialog.parentNode) { dialog.parentNode.removeChild(dialog); } }); } } /** * Show the controller management dialog */ function showControllerDialog() { // Implementation will be updated in a separate PR } // Export the dialog functions export { showLearningDialog, hideLearningDialog, showControllerDialog };