Files
2025-04-06 12:54:48 -04:00

517 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = `
<div class="cf-logo">
<span class="cf-logo-icon">🎮</span>
<span class="cf-logo-text">Control<span class="cf-logo-highlight">Freak</span></span>
</div>
`;
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 = `
<div><strong>Type:</strong> Node Widget</div>
<div><strong>Node:</strong> ${learningTarget.nodeTitle || learningTarget.nodeId}</div>
<div><strong>Widget:</strong> ${learningTarget.widgetName}</div>
`;
} else if (learningTarget.type === "ui_element") {
targetInfo.innerHTML = `
<div><strong>Type:</strong> UI Element</div>
<div><strong>Element ID:</strong> ${learningTarget.elementId}</div>
`;
} else if (learningTarget.type === "command") {
targetInfo.innerHTML = `
<div><strong>Type:</strong> Command</div>
<div><strong>Command:</strong> ${learningTarget.label || learningTarget.commandId}</div>
`;
} 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 = "<div class='controller-mapping-instruction'>Move a control on your controller to map it</div>";
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 = `
<option value="direct">Direct Control (Default)</option>
<option value="toggle">Toggle (On/Off)</option>
<option value="momentary">Momentary (Active while held)</option>
<option value="incremental">Increment Value</option>
<option value="decremental">Decrement Value</option>
<option value="trigger">Trigger Action</option>
`;
} else if (learningTarget.type === "command") {
// Command mapping types
typeSelect.innerHTML = `
<option value="trigger">Trigger Command (Default)</option>
<option value="toggle">Toggle Command</option>
<option value="momentary">Momentary (Active while held)</option>
`;
} else if (learningTarget.type === "ui_element") {
// UI element mapping types
typeSelect.innerHTML = `
<option value="trigger">Trigger Element (Default)</option>
<option value="toggle">Toggle Element</option>
<option value="momentary">Momentary (Active while held)</option>
`;
}
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 };