I went down a rabbit hole of creating keyboard assignments for windows.
Then I thought it would be cool if I could also use keyboard assignments to switch between instances,
i.e
I have a synth with a window that takes keyboard shortcuts
Have multiple versions of that synth controller window open
use some assignment like cmd+[numrowkey] to go to the relavent instance
Having been all round the shop I ran into what seems like a limitation with the way supercolliders focus works with the native (Mac) OS. All the code can be working beautifully, bringing the right windows to focus, and yet the keyboard focus can get stuck where the OS thinks it is, which is only shifted by OS switching (using OS keyboard shortcut or clicking with the mouse).
In the process of my debugging, I came up with a bunch of toy examples of increasing complexity to explore different solutions and highlight the problems.
There are 2 more pattern not included here, which I believe also won’t work
- having a master window to receive all the keys and delegate them to slave windows. This would run into all sorts of other weird issues of focus and scope.
- giving all windows globalEvent handlers and just getting them to recognise if the message is for them by setting an isFocused attribute on the object. This would run into parallel issues where the path of event bubbling becomes unpredictable, and keeping track of focus between the code and the OS becomes impossible.
If I had a really pressing need to implement the original idea I think it would probably have to be done at a lower level eg HID, which really doesn’t seem worth it.
Thought I’d share my journey, and the final solution which I’m pretty pleased with.
Let me know if I missed anything.
//////////////////////////////////////////////////////// >
// BASIC - KEYBOARD DOWN/UP ACTION MAPPINGS
//////////////////////////////////////////////////////// >
// debug window with TextView to show current key messages
(
// Debug function to see key presses
// creates a window to show current keydown and following up
~debugKeyMap = { |view, char, modifiers, unicode, keycode, textView|
var modifierText, messageText;
modifierText = "";
messageText = "";
// Decode modifier flags
if(modifiers & 1048576 > 0, { modifierText = modifierText ++ "Cmd+" });
if(modifiers & 131072 > 0, { modifierText = modifierText ++ "Shift+" });
if(modifiers & 262144 > 0, { modifierText = modifierText ++ "Alt+" });
if(modifiers & 524288 > 0, { modifierText = modifierText ++ "Ctrl+" });
messageText = messageText ++ ("\n=== KEY PRESS DEBUG ===");
messageText = messageText ++ ("\nCharacter: '" ++ char ++ "'");
messageText = messageText ++ ("\nUnicode: " ++ unicode);
messageText = messageText ++ ("\nKeycode: " ++ keycode);
messageText = messageText ++ ("\nModifiers raw: " ++ modifiers);
messageText = messageText ++ ("\nModifiers: " ++ modifierText);
messageText = messageText ++ ("\nIs digit: " ++ char.isDecDigit);
// override current string
textView.string_(messageText);
messageText.postln;
};
~debugKeyUpMap = { |view, char, modifiers, unicode, keycode ,textView|
var messageText = ("\n\nKEY UP: '" ++ char ++ "' modifiers: " ++ modifiers);
var currentString = textView.string;
// add the String rather than overriding
textView.string_(currentString++messageText);
messageText.postln;
("").postln;
};
// Create debug window
w = Window("Keyboard Debug", Rect(100, 100, 500, 400));
StaticText(w, Rect(20, 20, 460, 120))
.string_("Keyboard Debug Window\n\nPress any keys and check the post window\nfor detailed key information")
.font_(Font("Monaco", 14));
t = TextView(w, Rect(20, 150, 460, 200));
// assign keyDownAction to the function we want to use
w.view.keyDownAction = { |view, char, modifiers, unicode, keycode|
~debugKeyMap.(view, char, modifiers, unicode, keycode, t);
};
w.view.keyUpAction = { |view, char, modifiers, unicode, keycode|
~debugKeyUpMap.(view, char, modifiers, unicode, keycode, t);
};
w.front;
w.view.focus;
)
// WIDGET FOCUS WITHIN A WINDOW
//////////////////////////////////////////////////////// >
// Approach 1: Simple example with manual focus highlighting
//////////////////////////////////////////////////////// >
(
var window, textField1, textField2, button;
var focusedColor, unfocusedColor, focusedBorderColor, unfocusedBorderColor;
// Define colors
focusedColor = Color.yellow(0.3);
unfocusedColor = Color.white;
focusedBorderColor = Color.blue;
unfocusedBorderColor = Color.gray(0.7);
window = Window("Focus Demo - Simple", Rect(100, 100, 400, 200));
// TextField 1 with manual focus handling
textField1 = TextField(window, Rect(20, 20, 150, 30))
.string_("Field 1")
.background_(unfocusedColor);
textField1.focusGainedAction = {
textField1.background_(focusedColor);
("TextField 1 gained focus").postln;
};
textField1.focusLostAction = {
textField1.background_(unfocusedColor);
("TextField 1 lost focus").postln;
};
// TextField 2 with manual focus handling
textField2 = TextField(window, Rect(20, 60, 150, 30))
.string_("Field 2")
.background_(unfocusedColor);
textField2.focusGainedAction = {
textField2.background_(focusedColor);
("TextField 2 gained focus").postln;
};
textField2.focusLostAction = {
textField2.background_(unfocusedColor);
("TextField 2 lost focus").postln;
};
// Button (buttons don't have background color, so we'll use a different approach)
button = Button(window, Rect(20, 100, 100, 30))
.states_([["Click Me"]])
.focusColor_(focusedBorderColor);
button.focusGainedAction = {
("Button gained focus").postln;
};
button.focusLostAction = {
("Button lost focus").postln;
};
// Window focus tracking
window.onClose = { ("Window closed").postln; };
window.toFrontAction = { ("Window came to front").postln; };
window.front;
)
//////////////////////////////////////////////////////// >
// Approach 2: Comprehensive system with automatic focus
// highlighting with feedback using a dictionary of GUI elemetnts (widgets)
//////////////////////////////////////////////////////// >
(
var window, widgets, focusManager;
var createFocusManager, addFocusTracking;
// Focus manager that handles multiple widget types
createFocusManager = {
var manager, focusedColor, unfocusedColor, currentFocused;
focusedColor = Color.yellow(0.2);
unfocusedColor = Color.white;
currentFocused = nil;
manager = Dictionary.new;
// Method to add focus tracking to any widget
manager[\addWidget] = { |widget, name, statusWidget|
var originalBg;
// Store original background if it exists
originalBg = if(widget.respondsTo(\background), { widget.background }, { Color.white });
// Set up focus gained action
if(widget.respondsTo(\focusGainedAction), {
widget.focusGainedAction = {
// Clear previous focused widget
if(currentFocused.notNil, {
if(currentFocused[\widget].respondsTo(\background), {
currentFocused[\widget].background_(currentFocused[\originalBg]);
});
});
// Highlight current widget
if(widget.respondsTo(\background), {
widget.background_(focusedColor);
});
// Update tracking
currentFocused = (widget: widget, originalBg: originalBg, name: name);
("Focus gained: " ++ name).postln;
// Update status display in real-time
if(statusWidget.notNil, {
statusWidget.string_("Currently focused: " ++ name);
});
};
});
// Set up focus lost action
if(widget.respondsTo(\focusLostAction), {
widget.focusLostAction = {
if(widget.respondsTo(\background), {
widget.background_(originalBg);
});
("Focus lost: " ++ name).postln;
// Update status to show nothing focused
if(statusWidget.notNil, {
statusWidget.string_("Currently focused: None");
});
};
});
// Special handling for buttons (use focusColor instead)
if(widget.class == Button, {
widget.focusColor_(Color.blue);
});
("Added focus tracking to: " ++ name).postln;
};
// Method to get currently focused widget info
manager[\getCurrentFocus] = {
if(currentFocused.notNil, {
currentFocused[\name];
}, {
"None";
});
};
manager;
};
// Create the focus manager
focusManager = createFocusManager.value;
// Create window and widgets
window = Window("Focus Demo - Comprehensive", Rect(200, 200, 500, 350));
// Create various widget types
widgets = Dictionary.new;
widgets[\textField1] = TextField(window, Rect(20, 20, 150, 30))
.string_("Text Field 1");
widgets[\textField2] = TextField(window, Rect(200, 20, 150, 30))
.string_("Text Field 2");
widgets[\numberBox1] = NumberBox(window, Rect(20, 60, 80, 30))
.value_(42);
widgets[\numberBox2] = NumberBox(window, Rect(120, 60, 80, 30))
.value_(3.14);
widgets[\slider] = Slider(window, Rect(20, 100, 200, 30))
.value_(0.5);
widgets[\button1] = Button(window, Rect(20, 140, 100, 30))
.states_([["Button 1"]]);
widgets[\button2] = Button(window, Rect(140, 140, 100, 30))
.states_([["Button 2"]]);
widgets[\textView] = TextView(window, Rect(20, 180, 300, 80))
.string_("This is a TextView\nTry clicking here too");
// Status display
widgets[\statusText] = StaticText(window, Rect(20, 270, 400, 60))
.string_("Click on different widgets to see focus tracking\nCheck the post window for focus events")
.font_(Font("Arial", 12));
// Add focus tracking to all widgets
widgets.keysValuesDo({ |key, widget|
if(key != \statusText, { // Don't track the status text itself
focusManager[\addWidget].(widget, key.asString, widgets[\statusText]);
});
});
// Add a button to check current focus
widgets[\checkFocusBtn] = Button(window, Rect(370, 20, 100, 30))
.states_([["Check Focus"]])
.action_({
var current = focusManager[\getCurrentFocus].value;
("Currently focused: " ++ current).postln;
widgets[\statusText].string_("Currently focused: " ++ current);
});
focusManager[\addWidget].(widgets[\checkFocusBtn], "checkFocusBtn", widgets[\statusText]);
// Window-level focus tracking
window.toFrontAction = {
("Window gained focus").postln;
};
window.endFrontAction = {
("Window lost focus").postln;
};
window.front;
)
//////////////////////////////////////////////////////// >
// MULTI-WINDOW FOCUS TRACKING,
// with WIDGET FOCUS WITHIN A WINDOW
//////////////////////////////////////////////////////// >
(
var windows, widgets, focusManager, globalFocusState;
var createWindow, createFocusManager, createKeyDebugFunctions;
// Global state to track focus across all windows
globalFocusState = Dictionary.new;
globalFocusState[\activeWindow] = nil;
globalFocusState[\activeWidget] = nil;
// Enhanced focus manager that handles multiple windows
createFocusManager = {
var manager, focusedColor, unfocusedColor;
focusedColor = Color.yellow(0.2);
unfocusedColor = Color.white;
manager = Dictionary.new;
// Add widget focus tracking with window awareness
manager[\addWidget] = { |widget, name, statusWidget, windowName|
var originalBg;
originalBg = if(widget.respondsTo(\background), { widget.background }, { Color.white });
if(widget.respondsTo(\focusGainedAction), {
widget.focusGainedAction = {
// Update global focus state
globalFocusState[\activeWidget] = (name: name, window: windowName);
// Clear previous focused widget if it exists
if(manager[\currentFocused].notNil, {
if(manager[\currentFocused][\widget].respondsTo(\background), {
manager[\currentFocused][\widget].background_(manager[\currentFocused][\originalBg]);
});
});
// Highlight current widget
if(widget.respondsTo(\background), {
widget.background_(focusedColor);
});
// Update tracking
manager[\currentFocused] = (widget: widget, originalBg: originalBg, name: name, window: windowName);
("Widget focus gained: " ++ name ++ " in window: " ++ windowName).postln;
// Update all status displays
if(statusWidget.notNil, {
statusWidget.string_("Active: " ++ windowName ++ " -> " ++ name);
});
};
});
if(widget.respondsTo(\focusLostAction), {
widget.focusLostAction = {
if(widget.respondsTo(\background), {
widget.background_(originalBg);
});
("Widget focus lost: " ++ name ++ " in window: " ++ windowName).postln;
};
});
// Special handling for buttons
if(widget.class == Button, {
widget.focusColor_(Color.blue);
});
};
manager[\currentFocused] = nil;
manager;
};
// Keyboard debug functions
createKeyDebugFunctions = {
var debugKeyDown, debugKeyUp;
debugKeyDown = { |view, char, modifiers, unicode, keycode, textView, windowName|
var modifierText, messageText;
modifierText = "";
messageText = "";
// Decode modifier flags
if(modifiers & 1048576 > 0, { modifierText = modifierText ++ "Cmd+" });
if(modifiers & 131072 > 0, { modifierText = modifierText ++ "Shift+" });
if(modifiers & 262144 > 0, { modifierText = modifierText ++ "Alt+" });
if(modifiers & 524288 > 0, { modifierText = modifierText ++ "Ctrl+" });
messageText = messageText ++ ("=== " ++ windowName ++ " KEY DEBUG ===");
messageText = messageText ++ ("\nChar: '" ++ char ++ "' | Code: " ++ keycode);
messageText = messageText ++ ("\nModifiers: " ++ modifierText);
messageText = messageText ++ ("\nTime: " ++ Date.localtime.format("%H:%M:%S"));
textView.string_(messageText);
("Key received in " ++ windowName ++ ": '" ++ char ++ "'").postln;
};
debugKeyUp = { |view, char, modifiers, unicode, keycode, textView, windowName|
var currentString, keyUpMsg;
currentString = textView.string;
keyUpMsg = "\n\nKEY UP: '" ++ char ++ "'";
textView.string_(currentString ++ keyUpMsg);
("Key up in " ++ windowName ++ ": '" ++ char ++ "'").postln;
};
(keyDown: debugKeyDown, keyUp: debugKeyUp);
};
// Function to create a window with focus tracking
createWindow = { |windowName, rect|
var window, statusText, textField1, textField2, button, keyboardDebugView;
var windowDict, keyDebugFuncs;
keyDebugFuncs = createKeyDebugFunctions.value;
window = Window(windowName, rect);
// Window-level focus tracking
window.toFrontAction = {
globalFocusState[\activeWindow] = windowName;
("Window gained focus: " ++ windowName).postln;
// Update all status displays across all windows
windows.do({ |winDict|
if(winDict[\statusText].notNil, {
winDict[\statusText].string_("Active Window: " ++ windowName);
});
});
};
window.endFrontAction = {
("Window lost focus: " ++ windowName).postln;
// When window loses focus, we might still have a widget "focused"
// but it won't receive keyboard input
if(globalFocusState[\activeWidget].notNil, {
("Widget '" ++ globalFocusState[\activeWidget][\name] ++
"' may still appear focused but won't receive keyboard input").postln;
});
};
// Create widgets for this window
statusText = StaticText(window, Rect(10, 10, 350, 25))
.string_("Window: " ++ windowName ++ " | Click widgets to see focus tracking")
.font_(Font("Arial", 10))
.background_(Color.gray(0.9));
textField1 = TextField(window, Rect(10, 40, 120, 25))
.string_("Text Field 1");
textField2 = TextField(window, Rect(140, 40, 120, 25))
.string_("Text Field 2");
button = Button(window, Rect(270, 40, 80, 25))
.states_([["Button"]]);
// Keyboard debug view - shows which window actually receives keys
StaticText(window, Rect(10, 70, 350, 15))
.string_("Keyboard Input Debug (press any key):")
.font_(Font("Arial", 9));
keyboardDebugView = TextView(window, Rect(10, 90, 350, 80))
.string_("No keyboard input yet...")
.font_(Font("Monaco", 9))
.background_(Color.gray(0.95));
// Set up keyboard debugging for this window
window.view.keyDownAction = { |view, char, modifiers, unicode, keycode|
keyDebugFuncs[\keyDown].(view, char, modifiers, unicode, keycode, keyboardDebugView, windowName);
};
window.view.keyUpAction = { |view, char, modifiers, unicode, keycode|
keyDebugFuncs[\keyUp].(view, char, modifiers, unicode, keycode, keyboardDebugView, windowName);
};
// Button to test keyboard focus
Button(window, Rect(10, 175, 140, 25))
.states_([["Test Keyboard Input"]])
.action_({
("Testing keyboard focus in window: " ++ windowName).postln;
if(globalFocusState[\activeWindow] == windowName, {
("This window HAS OS focus - keyboard works").postln;
}, {
("This window may NOT have OS focus - keyboard may not work").postln;
});
});
// Button to show global focus state
Button(window, Rect(160, 175, 150, 25))
.states_([["Show Global Focus"]])
.action_({
("=== GLOBAL FOCUS STATE ===").postln;
("Active Window: " ++ globalFocusState[\activeWindow]).postln;
if(globalFocusState[\activeWidget].notNil, {
("Active Widget: " ++ globalFocusState[\activeWidget][\name] ++
" in " ++ globalFocusState[\activeWidget][\window]).postln;
}, {
("Active Widget: None").postln;
});
});
// Button to clear keyboard debug display
Button(window, Rect(320, 175, 40, 25))
.states_([["Clear"]])
.action_({
keyboardDebugView.string_("Cleared...");
});
// Create window dictionary
windowDict = Dictionary.new;
windowDict[\window] = window;
windowDict[\statusText] = statusText;
windowDict[\widgets] = Dictionary.new;
windowDict[\widgets][\textField1] = textField1;
windowDict[\widgets][\textField2] = textField2;
windowDict[\widgets][\button] = button;
windowDict;
};
// Create focus manager
focusManager = createFocusManager.value;
// Create multiple windows
windows = Array.new;
windows = windows.add(createWindow.value("Window A", Rect(100, 100, 380, 210)));
windows = windows.add(createWindow.value("Window B", Rect(500, 100, 380, 210)));
windows = windows.add(createWindow.value("Window C", Rect(300, 350, 380, 210)));
// Add focus tracking to all widgets in all windows
windows.do({ |winDict|
var windowName = winDict[\window].name;
winDict[\widgets].keysValuesDo({ |key, widget|
focusManager[\addWidget].(widget, key.asString, winDict[\statusText], windowName);
});
});
// Show all windows
windows.do({ |winDict| winDict[\window].front; });
// Set initial focus to first window
windows[0][\window].front;
("=== Multi-Window Focus Demo Started ===").postln;
("Try clicking between windows and widgets to see focus tracking").postln;
("Check the post window for detailed focus events").postln;
("Use 'Show Global Focus' buttons to see current state").postln;
)
//////////////////////////////////////////////////////// >
// LIMITATIONS OF NATIVE FOCUS SWITCHING WITH GLOBAL KEYMAPPING
// NEED Actual OS focus, gets out of sync when set through Supercollider Qt
//////////////////////////////////////////////////////// >
// Simple Window Switcher - Clear hierarchy approach
(
var win1, win2, win3;
// Simple Window Switcher - Clear hierarchy approach
var windowManager, createTestWindow;
// Simple window manager with clear focus handling
windowManager = (
windows: Dictionary.new,
activeKey: nil,
// Keycodes for number row (1-9)
numberKeycodes: (
18: 1, 19: 2, 20: 3, 21: 4, 23: 5,
22: 6, 26: 7, 28: 8, 25: 9
),
addWindow: { |self, key, window|
self.windows[key] = window;
// Set up window focus tracking
window.toFrontAction = {
("Window " ++ key ++ " came to front").postln;
self.activeKey = key;
self.updateFocusIndicators(key);
};
window.endFrontAction = {
("Window " ++ key ++ " lost front").postln;
};
// First window becomes active
if(self.activeKey.isNil, {
self.activeKey = key;
self.updateFocusIndicators(key);
});
},
switchToWindow: { |self, key|
var window, currentWindow;
window = self.windows[key];
if(window.notNil, {
("Switching to window: " ++ key).postln;
// First, explicitly remove focus from current window
if(self.activeKey.notNil, {
currentWindow = self.windows[self.activeKey];
if(currentWindow.notNil, {
currentWindow.view.focus(false);
("Removed focus from: " ++ self.activeKey).postln;
});
});
// Then bring new window to front and focus it
window.front; // OS front
window.view.focus(true); // Internal focus - may not work reliably
("Set focus to: " ++ key).postln;
self.activeKey = key;
// Update visual indicators
self.updateFocusIndicators(key);
// Show limitation message
("LIMITATION: You may need to click in the window to establish keyboard focus").postln;
}, {
("Window " ++ key ++ " not found").postln;
});
},
updateFocusIndicators: { |self, activeKey|
self.windows.keysValuesDo({ |key, window|
var statusWidget = window.view.children[0]; // First child is status text
if(key == activeKey, {
statusWidget.background_(Color.red(0.3));
statusWidget.string_("Window: " ++ key ++ " [SHOULD HAVE FOCUS - click if needed]");
}, {
statusWidget.background_(Color.gray(0.7));
statusWidget.string_("Window: " ++ key ++ " [inactive]");
});
});
},
handleGlobalKey: { |self, keycode, modifiers|
var windowNumber = self.numberKeycodes[keycode];
var isCmdPressed = (modifiers & 1048576) > 0;
// Debug what we're seeing
("Global handler - keycode: " ++ keycode ++ ", windowNumber: " ++ windowNumber ++ ", isCmdPressed: " ++ isCmdPressed).postln;
// ONLY consume Cmd+number combinations
if(windowNumber.notNil && isCmdPressed, {
var windowKeys = self.windows.keys.asArray.sort;
if(windowNumber <= windowKeys.size, {
var targetKey = windowKeys[windowNumber - 1];
("Consuming global key - switching to: " ++ targetKey).postln;
self.switchToWindow(targetKey);
^true; // Event consumed - this was a window switch command
});
});
("Not consuming - passing to local handler").postln;
false; // Event NOT consumed - let it go to local handler
}
);
// Function to create a test window
createTestWindow = { |name, rect, color|
var window, textField, keyDebugView, statusText;
window = Window(name, rect);
// Status display
statusText = StaticText(window, Rect(10, 10, 300, 20))
.string_("Window: " ++ name)
.background_(color)
.font_(Font("Arial", 12));
// Text field for testing focus
/*textField = TextField(window, Rect(10, 40, 200, 25))
.string_("Type here to test focus");
*/
// Keyboard debug view
keyDebugView = TextView(window, Rect(10, 75, 300, 100))
.string_("Keyboard debug - press keys...")
.font_(Font("Monaco", 10))
.background_(Color.gray(0.95))
.editable_(false);
// Window-level keyboard handling
window.view.keyDownAction = { |view, char, modifiers, unicode, keycode|
var consumed = false;
// First, try global window switching
consumed = windowManager.handleGlobalKey(keycode, modifiers);
("Local handler in " ++ name ++ " - consumed: " ++ consumed).postln;
// If not consumed by global handler, handle locally
if(consumed.not, {
var modStr = "";
var debugText;
("Running local handler for: " ++ char).postln;
if((modifiers & 1048576) > 0, { modStr = modStr ++ "Cmd+" });
if((modifiers & 131072) > 0, { modStr = modStr ++ "Shift+" });
if((modifiers & 262144) > 0, { modStr = modStr ++ "Alt+" });
if((modifiers & 524288) > 0, { modStr = modStr ++ "Ctrl+" });
debugText = "Key in " ++ name ++ ":\n";
debugText = debugText ++ "Char: '" ++ char ++ "'\n";
debugText = debugText ++ "Keycode: " ++ keycode ++ "\n";
debugText = debugText ++ "Modifiers: " ++ modStr ++ "\n";
debugText = debugText ++ "Time: " ++ Date.localtime.format("%H:%M:%S");
keyDebugView.string_(debugText);
("Local key in " ++ name ++ ": '" ++ char ++ "'").postln;
}, {
("Global handler consumed the event").postln;
});
};
// Add click handler to establish focus when clicking anywhere in window
window.view.mouseDownAction = { |view, x, y, modifiers, buttonNumber, clickCount|
("Clicked in " ++ name ++ " - establishing focus").postln;
windowManager.activeKey = name.asSymbol;
windowManager.updateFocusIndicators(name.asSymbol);
};
// Clear button
Button(window, Rect(220, 40, 60, 25))
.states_([["Clear"]])
.action_({ keyDebugView.string_("Cleared..."); });
// Button to manually switch windows
Button(window, Rect(10, 185, 100, 25))
.states_([["Switch Test"]])
.action_({
var keys = windowManager.windows.keys.asArray;
var otherKeys = keys.reject({ |k| k == name.asSymbol });
if(otherKeys.size > 0, {
windowManager.switchToWindow(otherKeys[0]);
});
});
// Instructions
StaticText(window, Rect(10, 220, 300, 60))
.string_("Cmd+1/2/3: Switch windows\nType in text field to test focus\n\nLIMITATION: Click in window after switching\nto establish keyboard focus")
.font_(Font("Arial", 10))
.background_(Color.yellow(0.1));
window;
};
// Create test windows
win1 = createTestWindow.value("Window 1", Rect(100, 100, 330, 300), Color.yellow(0.3));
win2 = createTestWindow.value("Window 2", Rect(450, 100, 330, 300), Color.blue(0.3));
win3 = createTestWindow.value("Window 3", Rect(275, 420, 330, 300), Color.green(0.3));
// Add to window manager
windowManager.addWindow(\win1, win1);
windowManager.addWindow(\win2, win2);
windowManager.addWindow(\win3, win3);
// Show all windows
win1.front;
win2.front;
win3.front;
// Set focus to first window
windowManager.switchToWindow(\win1);
("=== Complete Window Switcher Example ===").postln;
("WHAT WORKS:").postln;
("- Cmd+1/2/3 switches windows visually").postln;
("- Local keyboard handling works when focus is established").postln;
("- Visual indicators show intended focus state").postln;
("- Click detection helps establish focus").postln;
("").postln;
("LIMITATION:").postln;
("- Programmatic focus transfer is unreliable").postln;
("- You must CLICK in a window after switching to establish keyboard focus").postln;
("- This is a SuperCollider limitation, not a bug in our code").postln;
("").postln;
("TRY THIS:").postln;
("1. Press Cmd+1 to switch to Window 1").postln;
("2. Click anywhere in Window 1 to establish focus").postln;
("3. Press some keys - they should appear in Window 1's debug area").postln;
("4. Repeat with Cmd+2/3 for other windows").postln;
)