import { Doc, DocListCast } from '../../../../../fields/Doc'; import { ScriptField } from '../../../../../fields/ScriptField'; import { Cast, PromiseValue } from '../../../../../fields/Types'; import { InkInkTool, InkTool } from '../../../../../fields/InkField'; import { MainView } from '../../../MainView'; import { DocumentView } from '../../DocumentView'; import { RichTextMenu } from '../../formattedText/RichTextMenu'; import { PropertiesView } from '../../../PropertiesView'; import { CollectionViewType } from '../../../../documents/DocumentTypes'; import { ParametersType, ToolInfo } from '../types/tool_types'; import { Observation } from '../types/types'; import { BaseTool } from './BaseTool'; const uiControlParams = [ { name: 'action', type: 'string', description: 'The UI action to perform. Options: "open_tab" (Files, Tools, Imports), "close_tab" (closes current sidebar tab), "select_tool" (pen, highlighter, eraser, text, circle, etc.), "change_font_size", "select_font", "switch_view", "toggle_tags", "toggle_properties_submenu", "toggle_properties", "toggle_header"', required: true, }, { name: 'target', type: 'string', description: 'The target of the action. For open_tab: "Files", "Tools", "Imports", "Trails", "Search", "Properties". For select_tool: "pen", "highlighter", "write", "math", "eraser", "text", "ink", "none". For select_font: "Arial", "Comic Sans MS", etc. For change_font_size: number as string. For switch_view: "freeform", "card", "carousel", "stacking", etc. For toggle_properties_submenu: "options", "fields", "appearance", "layout", "sharing", etc.', required: false, }, ] as const; type UIControlToolParamsType = typeof uiControlParams; const uiControlToolInfo: ToolInfo = { name: 'uiControl', citationRules: 'No citation needed for UI control actions.', parameterRules: uiControlParams, description: 'Control the Dash UI by opening/closing tabs, selecting tools, changing fonts, switching views, toggling tags, and managing properties sub-menus. Supports: tab management (Files, Tools, Properties, etc.), tool dropdowns (ink button opens pen/highlighter/write/math options, text button opens font/size/color options), font changes, view switching (freeform, card, etc.), tag visibility toggle, and properties panel sub-menu control (options, fields, appearance, etc.).', }; export class UIControlTool extends BaseTool { constructor() { super(uiControlToolInfo); } async execute(args: ParametersType): Promise { const { action, target } = args; try { let result = ''; switch (action) { case 'open_tab': result = await this.openTab(target || ''); break; case 'close_tab': result = this.closeTab(); break; case 'select_tool': result = await this.selectTool(target || ''); break; case 'change_font_size': result = await this.changeFontSize(String(target || '')); break; case 'select_font': result = await this.selectFont(target || ''); break; case 'toggle_properties': result = this.toggleProperties(); break; case 'toggle_header': result = this.toggleHeader(); break; case 'switch_view': result = this.switchView(target || ''); break; case 'toggle_tags': result = this.toggleTags(); break; case 'toggle_properties_submenu': result = this.togglePropertiesSubmenu(target || ''); break; default: result = `Unknown action: ${action}`; } return [ { type: 'text', text: result, }, ]; } catch (error) { console.error('UIControlTool error:', error); return [ { type: 'text', text: `Error performing UI action: ${error}`, }, ]; } } private async openTab(tab: string): Promise { console.log(`[UIControlTool] Attempting to open tab: ${tab}`); const mainView = MainView.Instance; if (!mainView) { console.error('[UIControlTool] MainView.Instance is not available'); return 'MainView not available'; } const normalizedTab = tab.toLowerCase(); console.log(`[UIControlTool] Normalized tab name: ${normalizedTab}`); try { switch (normalizedTab) { case 'files': case 'filesystem': console.log('[UIControlTool] Trying to open Files tab'); const sidebarMenu = Doc.MyLeftSidebarMenu; console.log('[UIControlTool] MyLeftSidebarMenu:', sidebarMenu); if (sidebarMenu?.data) { const menuItems = DocListCast(sidebarMenu.data); console.log('[UIControlTool] Menu items count:', menuItems.length); const filesBtn = menuItems.find(d => d.target === Doc.MyFilesystem); console.log('[UIControlTool] Files button found:', !!filesBtn); if (filesBtn) { mainView.selectLeftSidebarButton(filesBtn); return 'Opened Files tab'; } else { return 'Files button not found in sidebar menu'; } } else { return 'Sidebar menu data not available'; } case 'tools': console.log('[UIControlTool] Trying to open Tools tab'); const toolsBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyTools); console.log('[UIControlTool] Tools button found:', !!toolsBtn); if (toolsBtn) { mainView.selectLeftSidebarButton(toolsBtn); return 'Opened Tools tab'; } else { return 'Tools button not found in sidebar menu'; } case 'imports': console.log('[UIControlTool] Trying to open Imports tab'); const importBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyImports); console.log('[UIControlTool] Import button found:', !!importBtn); if (importBtn) { mainView.selectLeftSidebarButton(importBtn); return 'Opened Imports tab'; } else { return 'Imports button not found in sidebar menu'; } case 'trails': case 'presentations': console.log('[UIControlTool] Trying to open Trails tab'); const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyTrails); console.log('[UIControlTool] Trails button found:', !!trailsBtn); if (trailsBtn) { mainView.selectLeftSidebarButton(trailsBtn); return 'Opened Trails/Presentations tab'; } else { return 'Trails button not found in sidebar menu'; } case 'search': console.log('[UIControlTool] Trying to open Search tab'); const searchBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MySearcher); console.log('[UIControlTool] Search button found:', !!searchBtn); if (searchBtn) { mainView.selectLeftSidebarButton(searchBtn); return 'Opened Search tab'; } else { return 'Search button not found in sidebar menu'; } case 'properties': case 'property': console.log('[UIControlTool] Trying to open Properties panel'); // Properties is not a sidebar tab, it's a right-side panel mainView.togglePropertiesFlyout(); return 'Opened Properties panel'; default: return `Unknown tab: ${tab}. Available: Files, Tools, Imports, Trails, Search, Properties`; } } catch (error) { console.error(`[UIControlTool] Error opening tab ${tab}:`, error); return `Error opening tab ${tab}: ${error}`; } } private closeTab(): string { const mainView = MainView.Instance; if (!mainView) { return 'MainView not available'; } try { mainView.closeFlyout(); return 'Closed sidebar tab'; } catch (error) { console.error('[UIControlTool] Error closing tab:', error); return `Error closing tab: ${error}`; } } private async selectTool(tool: string): Promise { const normalizedTool = tool.toLowerCase(); console.log(`[UIControlTool] Selecting tool: ${normalizedTool}`); try { switch (normalizedTool) { case 'pen': case 'pen-nib': Doc.ActiveInk = InkInkTool.Pen; Doc.ActiveTool = InkTool.Ink; console.log(`[UIControlTool] Set ActiveTool to ${Doc.ActiveTool}, ActiveInk to ${Doc.ActiveInk}`); return 'Selected pen tool'; case 'highlighter': case 'highlight': Doc.ActiveInk = InkInkTool.Highlight; Doc.ActiveTool = InkTool.Ink; console.log(`[UIControlTool] Set ActiveTool to ${Doc.ActiveTool}, ActiveInk to ${Doc.ActiveInk}`); return 'Selected highlighter tool'; case 'write': case 'writing': case 'handwriting': Doc.ActiveInk = InkInkTool.Write; Doc.ActiveTool = InkTool.Ink; console.log(`[UIControlTool] Set ActiveTool to ${Doc.ActiveTool}, ActiveInk to ${Doc.ActiveInk}`); return 'Selected handwriting tool'; case 'math': case 'calculator': Doc.ActiveInk = InkInkTool.Math; Doc.ActiveTool = InkTool.Ink; console.log(`[UIControlTool] Set ActiveTool to ${Doc.ActiveTool}, ActiveInk to ${Doc.ActiveInk}`); return 'Selected math tool'; case 'eraser': Doc.ActiveTool = InkTool.Eraser; console.log(`[UIControlTool] Set ActiveTool to ${Doc.ActiveTool}`); return 'Selected eraser tool'; case 'text': case 'text tool': case 'text button': // Text button should open the text formatting dropdown, not just select text tool // First ensure Tools panel is open const mainView = MainView.Instance; if (mainView) { const toolsBtn = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyTools); if (toolsBtn) { mainView.selectLeftSidebarButton(toolsBtn); console.log(`[UIControlTool] Opened Tools panel for text button`); // The text dropdown should open automatically when Tools panel is opened // and text documents are selected. This matches existing UI behavior. return 'Opened text button dropdown in Tools panel. You can now access font, size, color, and other text formatting options.'; } } return 'Could not open text button dropdown - Tools panel not available'; case 'ink': case 'ink tool': case 'ink button': // Ink button should open the ink tools dropdown with pen, highlighter, write, math options const mainViewInk = MainView.Instance; if (mainViewInk) { const toolsBtnInk = DocListCast(Doc.MyLeftSidebarMenu?.data).find(d => d.target === Doc.MyTools); if (toolsBtnInk) { mainViewInk.selectLeftSidebarButton(toolsBtnInk); console.log(`[UIControlTool] Opened Tools panel for ink button`); // The ink dropdown should open automatically when Tools panel is opened // This matches existing UI behavior for MultiToggleButton types return 'Opened ink button dropdown in Tools panel. You can now access pen, highlighter, write, and math tools.'; } } return 'Could not open ink button dropdown - Tools panel not available'; case 'none': case 'select': case 'selection': Doc.ActiveTool = InkTool.None; console.log(`[UIControlTool] Set ActiveTool to ${Doc.ActiveTool} (selection tool)`); return 'Selected selection tool'; case 'circle': case 'shape': // TODO: Implement shape tool selection when we find the API return 'Shape tool selection not yet implemented'; default: return `Unknown tool: ${tool}. Available tools: pen, highlighter, write, math, eraser, text, circle`; } } catch (error) { console.error('[UIControlTool] Error selecting tool:', error); return `Error selecting tool: ${error}`; } } private async selectFont(fontName: string): Promise { if (!fontName) { return 'Font name is required'; } try { // First ensure text tool is selected Doc.ActiveTool = InkTool.None; // Validate font name const validFonts = ['Roboto', 'Roboto Mono', 'Nunito', 'Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text', 'Math']; const normalizedFont = validFonts.find(font => font.toLowerCase() === fontName.toLowerCase() || font.toLowerCase().includes(fontName.toLowerCase()) ); if (!normalizedFont) { return `Font "${fontName}" not found. Available fonts: ${validFonts.join(', ')}`; } // Try to set font using RichTextMenu if (RichTextMenu.Instance) { RichTextMenu.Instance.setFontField(normalizedFont, 'fontFamily'); return `Selected font: ${normalizedFont}`; } else { // Fallback: Set on user document Doc.UserDoc().fontFamily = normalizedFont; return `Set default font to: ${normalizedFont}`; } } catch (error) { console.error('[UIControlTool] Error selecting font:', error); return `Error selecting font: ${error}`; } } private changeFontSize(size: string): string { const fontSize = parseInt(size); if (isNaN(fontSize) || fontSize < 1 || fontSize > 200) { return 'Invalid font size. Please provide a number between 1 and 200.'; } // Get selected text documents const selectedViews = DocumentView.Selected(); if (selectedViews.length === 0) { return 'No text document selected. Please select a text document first.'; } let changedCount = 0; selectedViews.forEach(view => { const doc = view.Document; if (doc.type === 'rich text' || doc.type === 'text') { // TODO: Find the correct property for font size // This is a placeholder - need to find actual implementation doc.$fontSize = fontSize; changedCount++; } }); if (changedCount > 0) { return `Changed font size to ${fontSize} for ${changedCount} document(s)`; } else { return 'No text documents selected to change font size'; } } private toggleProperties(): string { MainView.Instance?.togglePropertiesFlyout(); return 'Toggled properties panel'; } private toggleHeader(): string { MainView.Instance?.toggleTopBar(); return 'Toggled header bar'; } private switchView(viewType: string): string { try { // Use the existing setView function from globalScripts, which handles view switching properly const normalizedView = viewType.toLowerCase(); let mappedViewType: string; // Map common view names to CollectionViewType values switch (normalizedView) { case 'freeform': case 'free form': mappedViewType = CollectionViewType.Freeform; break; case 'card': case 'card view': mappedViewType = CollectionViewType.Card; break; case 'carousel': mappedViewType = CollectionViewType.Carousel; break; case '3d carousel': case 'carousel3d': mappedViewType = CollectionViewType.Carousel3D; break; case 'stacking': case 'stack': mappedViewType = CollectionViewType.Stacking; break; case 'grid': mappedViewType = CollectionViewType.Grid; break; case 'tree': mappedViewType = CollectionViewType.Tree; break; case 'masonry': mappedViewType = CollectionViewType.Masonry; break; case 'notetaking': case 'note taking': mappedViewType = CollectionViewType.NoteTaking; break; case 'schema': mappedViewType = CollectionViewType.Schema; break; default: return `Unknown view type: ${viewType}. Available: freeform, card, carousel, 3d carousel, stacking, grid, tree, masonry, notetaking, schema`; } // Apply view change directly to selected document, mirroring setView function logic const selected = DocumentView.Selected().lastElement(); if (!selected) { return 'No documents selected to switch view'; } // Apply the view change (like setView function does) if (selected.Document.type === 'collection') { selected.Document._type_collection = mappedViewType; return `Successfully switched to ${viewType} view`; } else { return 'Selected document is not a collection, cannot switch view'; } } catch (error) { console.error('[UIControlTool] Error switching view:', error); return `Error switching view: ${error}`; } } private toggleTags(): string { try { // Use the exact same logic as DocumentButtonBar keywordButton const selectedDocs = DocumentView.Selected(); if (selectedDocs.length === 0) { return 'No documents selected to toggle tags'; } // Check if ANY document is currently showing tags (like DocumentButtonBar does) const showing = selectedDocs.some(dv => dv.showTags); // Set ALL documents to the OPPOSITE state (like DocumentButtonBar does) selectedDocs.forEach(dv => { dv.layoutDoc._layout_showTags = !showing; }); const newState = !showing; return `${newState ? 'Enabled' : 'Disabled'} tag display for ${selectedDocs.length} document(s)`; } catch (error) { console.error('[UIControlTool] Error toggling tags:', error); return `Error toggling tags: ${error}`; } } private togglePropertiesSubmenu(submenuName: string): string { try { const propertiesView = PropertiesView.Instance; if (!propertiesView) { return 'Properties panel is not available'; } const normalizedName = submenuName.toLowerCase(); let toggledSubmenu = ''; switch (normalizedName) { case 'options': propertiesView.openOptions = !propertiesView.openOptions; toggledSubmenu = 'Options'; break; case 'fields': case 'fields & tags': propertiesView.openFields = !propertiesView.openFields; toggledSubmenu = 'Fields & Tags'; break; case 'appearance': propertiesView.openAppearance = !propertiesView.openAppearance; toggledSubmenu = 'Appearance'; break; case 'layout': propertiesView.openLayout = !propertiesView.openLayout; toggledSubmenu = 'Layout'; break; case 'sharing': propertiesView.openSharing = !propertiesView.openSharing; toggledSubmenu = 'Sharing'; break; case 'links': propertiesView.openLinks = !propertiesView.openLinks; toggledSubmenu = 'Links'; break; case 'contexts': case 'other contexts': propertiesView.openContexts = !propertiesView.openContexts; toggledSubmenu = 'Other Contexts'; break; case 'filters': propertiesView.openFilters = !propertiesView.openFilters; toggledSubmenu = 'Filters'; break; case 'transform': propertiesView.openTransform = !propertiesView.openTransform; toggledSubmenu = 'Transform'; break; case 'firefly': propertiesView.openFirefly = !propertiesView.openFirefly; toggledSubmenu = 'Firefly'; break; case 'styling': propertiesView.openStyling = !propertiesView.openStyling; toggledSubmenu = 'Styling'; break; default: return `Unknown submenu: ${submenuName}. Available: options, fields, appearance, layout, sharing, links, contexts, filters, transform, firefly, styling`; } const currentState = this.getSubmenuState(propertiesView, normalizedName); return `${currentState ? 'Opened' : 'Closed'} ${toggledSubmenu} submenu in Properties panel`; } catch (error) { console.error('[UIControlTool] Error toggling properties submenu:', error); return `Error toggling properties submenu: ${error}`; } } private getSubmenuState(propertiesView: PropertiesView, submenuName: string): boolean { switch (submenuName) { case 'options': return propertiesView.openOptions; case 'fields': return propertiesView.openFields; case 'appearance': return propertiesView.openAppearance; case 'layout': return propertiesView.openLayout; case 'sharing': return propertiesView.openSharing; case 'links': return propertiesView.openLinks; case 'contexts': return propertiesView.openContexts; case 'filters': return propertiesView.openFilters; case 'transform': return propertiesView.openTransform; case 'firefly': return propertiesView.openFirefly; case 'styling': return propertiesView.openStyling; default: return false; } } }