/**
* 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 = `
🎮
ControlFreak
`;
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 };