@teachinglab/omd
Version:
omd
394 lines (344 loc) • 12.1 kB
JavaScript
export class ToolManager {
/**
* @param {OMDCanvas} canvas - Canvas instance
*/
constructor(canvas) {
this.canvas = canvas;
this.tools = new Map();
this.activeTool = null;
this.previousTool = null;
this.isDestroyed = false;
}
/**
* Register a tool with the manager
* @param {string} name - Tool name
* @param {Tool} tool - Tool instance
* @returns {boolean} True if tool was registered successfully
*/
registerTool(name, tool) {
if (this.isDestroyed) {
console.warn('Cannot register tool on destroyed ToolManager');
return false;
}
if (!name || typeof name !== 'string') {
console.error('Tool name must be a non-empty string');
return false;
}
if (!tool || typeof tool.onPointerDown !== 'function') {
console.error('Tool must implement required methods');
return false;
}
// Check if tool is enabled in config
if (!this.canvas.config.enabledTools.includes(name)) {
console.warn(`Tool '${name}' is not enabled in canvas configuration`);
return false;
}
// Set tool name and canvas reference
tool.name = name;
tool.canvas = this.canvas;
// Store tool
this.tools.set(name, tool);
console.log(`Tool '${name}' registered successfully`);
return true;
}
/**
* Unregister a tool
* @param {string} name - Tool name
* @returns {boolean} True if tool was unregistered
*/
unregisterTool(name) {
const tool = this.tools.get(name);
if (!tool) return false;
// Deactivate if it's the active tool
if (this.activeTool === tool) {
this.setActiveTool(null);
}
// Remove from tools
this.tools.delete(name);
console.log(`Tool '${name}' unregistered`);
return true;
}
/**
* Set the active tool
* @param {string|null} toolName - Tool name to activate, or null to deactivate all
* @returns {boolean} True if tool was activated successfully
*/
setActiveTool(toolName) {
// Deactivate current tool
if (this.activeTool) {
try {
this.activeTool.onDeactivate();
} catch (error) {
console.error('Error deactivating tool:', error);
}
this.previousTool = this.activeTool;
}
// Clear active tool if null
if (!toolName) {
this.activeTool = null;
this.canvas.emit('toolChanged', { name: null, tool: null, previous: this.previousTool?.name });
return true;
}
// Get new tool
const newTool = this.tools.get(toolName);
if (!newTool) {
console.error(`Tool '${toolName}' not found`);
return false;
}
// Activate new tool
this.activeTool = newTool;
try {
this.activeTool.onActivate();
} catch (error) {
console.error('Error activating tool:', error);
this.activeTool = null;
return false;
}
// Update cursor if available
if (this.canvas.cursor && this.activeTool.getCursor) {
const cursorType = this.activeTool.getCursor();
this.canvas.cursor.setShape(cursorType);
}
// Update tool config for cursor
if (this.canvas.cursor && this.activeTool.config) {
this.canvas.cursor.updateFromToolConfig(this.activeTool.config);
}
// Emit tool change event
this.canvas.emit('toolChanged', {
name: toolName,
tool: newTool,
previous: this.previousTool?.name
});
console.log(`Tool '${toolName}' activated`);
return true;
}
/**
* Get the currently active tool
* @returns {Tool|null} Active tool instance
*/
getActiveTool() {
return this.activeTool;
}
/**
* Get tool by name
* @param {string} name - Tool name
* @returns {Tool|undefined} Tool instance
*/
getTool(name) {
return this.tools.get(name);
}
/**
* Get all registered tool names
* @returns {Array<string>} Array of tool names
*/
getToolNames() {
return Array.from(this.tools.keys());
}
/**
* Get all registered tools
* @returns {Map<string, Tool>} Map of tools
*/
getAllTools() {
return new Map(this.tools);
}
/**
* Get metadata for all tools
* @returns {Array<Object>} Array of tool metadata
*/
getAllToolMetadata() {
return Array.from(this.tools.entries()).map(([name, tool]) => ({
name,
displayName: tool.displayName || name,
description: tool.description || '',
shortcut: tool.shortcut || '',
category: tool.category || 'general',
icon: tool.icon || 'tool'
}));
}
/**
* Switch to previous tool
* @returns {boolean} True if switched successfully
*/
switchToPreviousTool() {
if (this.previousTool) {
return this.setActiveTool(this.previousTool.name);
}
return false;
}
/**
* Temporarily switch to a tool and back
* @param {string} toolName - Tool to switch to temporarily
* @param {Function} callback - Function to execute with temporary tool
* @returns {Promise<any>} Result of callback
*/
async withTemporaryTool(toolName, callback) {
const currentTool = this.activeTool?.name;
if (!this.setActiveTool(toolName)) {
throw new Error(`Failed to activate temporary tool: ${toolName}`);
}
try {
const result = await callback(this.activeTool);
return result;
} finally {
// Restore previous tool
if (currentTool) {
this.setActiveTool(currentTool);
}
}
}
/**
* Update tool configuration
* @param {string} toolName - Tool name
* @param {Object} config - Configuration updates
* @returns {boolean} True if updated successfully
*/
updateToolConfig(toolName, config) {
const tool = this.tools.get(toolName);
if (!tool) {
console.error(`Tool '${toolName}' not found`);
return false;
}
if (tool.updateConfig) {
tool.updateConfig(config);
// Update cursor if this is the active tool
if (this.activeTool === tool && this.canvas.cursor) {
this.canvas.cursor.updateFromToolConfig(tool.config);
}
return true;
}
console.warn(`Tool '${toolName}' does not support configuration updates`);
return false;
}
/**
* Get tool configuration
* @param {string} toolName - Tool name
* @returns {Object|null} Tool configuration
*/
getToolConfig(toolName) {
const tool = this.tools.get(toolName);
return tool ? tool.config || {} : null;
}
/**
* Check if a tool is registered
* @param {string} toolName - Tool name
* @returns {boolean} True if tool is registered
*/
hasTool(toolName) {
return this.tools.has(toolName);
}
/**
* Check if a tool is enabled in configuration
* @param {string} toolName - Tool name
* @returns {boolean} True if tool is enabled
*/
isToolEnabled(toolName) {
return this.canvas.config.enabledTools.includes(toolName);
}
/**
* Get tool capabilities
* @param {string} toolName - Tool name
* @returns {Object|null} Tool capabilities
*/
getToolCapabilities(toolName) {
const tool = this.tools.get(toolName);
if (!tool) return null;
return {
name: tool.name,
displayName: tool.displayName,
description: tool.description,
shortcut: tool.shortcut,
category: tool.category,
supportsKeyboardShortcuts: typeof tool.onKeyboardShortcut === 'function',
supportsPressure: tool.supportsPressure || false,
supportsMultiTouch: tool.supportsMultiTouch || false,
configurable: typeof tool.updateConfig === 'function',
hasHelp: typeof tool.getHelpText === 'function'
};
}
/**
* Handle keyboard shortcuts for tools
* @param {string} key - Key pressed
* @param {KeyboardEvent} event - Keyboard event
* @returns {boolean} True if shortcut was handled
*/
handleKeyboardShortcut(key, event) {
// First, check for tool switching shortcuts
for (const [name, tool] of this.tools) {
if (tool.shortcut && tool.shortcut.toLowerCase() === key.toLowerCase()) {
this.setActiveTool(name);
return true;
}
}
// Then, delegate to active tool
if (this.activeTool && this.activeTool.onKeyboardShortcut) {
return this.activeTool.onKeyboardShortcut(key, event);
}
return false;
}
/**
* Get help text for all tools or specific tool
* @param {string} [toolName] - Optional tool name
* @returns {string|Object} Help text
*/
getHelpText(toolName = null) {
if (toolName) {
const tool = this.tools.get(toolName);
if (tool && tool.getHelpText) {
return tool.getHelpText();
}
return `No help available for tool: ${toolName}`;
}
// Return help for all tools
const helpTexts = {};
for (const [name, tool] of this.tools) {
if (tool.getHelpText) {
helpTexts[name] = tool.getHelpText();
}
}
return helpTexts;
}
/**
* Get current tool manager state
* @returns {Object} Current state
*/
getState() {
return {
activeToolName: this.activeTool?.name || null,
previousToolName: this.previousTool?.name || null,
registeredTools: this.getToolNames(),
enabledTools: this.canvas.config.enabledTools,
isDestroyed: this.isDestroyed
};
}
/**
* Destroy the tool manager
*/
destroy() {
if (this.isDestroyed) return;
// Deactivate current tool
if (this.activeTool) {
try {
this.activeTool.onDeactivate();
} catch (error) {
console.error('Error deactivating tool during destroy:', error);
}
}
// Destroy all tools if they have a destroy method
for (const [name, tool] of this.tools) {
if (tool.destroy) {
try {
tool.destroy();
} catch (error) {
console.error(`Error destroying tool '${name}':`, error);
}
}
}
// Clear references
this.tools.clear();
this.activeTool = null;
this.previousTool = null;
this.canvas = null;
this.isDestroyed = true;
console.log('ToolManager destroyed');
}
}