This app is essentially a text-based Command+Tab that works how I wish that command worked. Right now the keyboard shortcut is Option+Tab.
-
Pressing Option-Tab from any application while TextSwitcher is open displays a window that allows the user to filter a list of open windows by partial text-matching against the window title or application name.
-
Pressing Option-Tab activates TextSwitcher from anywhere in the system (a global hot-key).
-
Typing text in the open TextSwitcher window filters the list of windows by application name or window title.
-
Pressing Command and the number key displayed next to an item in the list brings that window to the foreground.
-
Pressing Enter brings the top-most window in the list to the foreground.
-
Pressing Escape hides the app and returns you to the last window you were on.
AppDelegate
sets up the app's global system hotkey using the MASShortcut library.
TODO: A preference pane allows the user to edit the keyboard shortcut.
When the app is on screen, a nameless view containing a search input and a text
box are displayed. These classes are all defined in Main.storyboard
using
Interface Builder, with the exception of the search control, which is an
instance of TextSwitcherView
.
TextSwitcherView
is a NSSearchField
subclass that listens for
Command+number key presses when the search field is focused. If it detects
a Command+number key press, it sends the chooseSearchResult:
message
up the responder chain with the number pressed.
TODO: Should a protocol define this interface? What's the Cocoa pattern?
ViewController
is responsible for setting up the list of windows that appears
in the nameless text box in the app's main view, changing the list depending on
text that the user provides in the search input, and handling the message
that a user chose a window to open.
ViewController
uses AccessibilityWrapper
to get a list of windows on screen
using the Cocoa API call CGWindowListCopyWindowInfo
. It populates a TableView
with this data, including hints for the keyboard shortcuts that the user can
press to activate each window.
The controller receives text that the user searches for because it has an
IBAction
wired up to the NSSearchFieldCell
in the app's storyboard.
When this happens, the controller filters the list of windows to those that have the text that the user typed in their name or owner name (like "Firefox" for the owner or the name of a particular window - usually the currently-displayed tab).
A one-indexed number like (1) or (2) is displayed next to each window in the search results box. By pressing Command+number, the user can select a window from the list to bring it on screen.
The user can press Enter to select the top-most item in the list, or Command+number to select an item by its index in the list.
TextSwitcherView
accomplishes listening to these key presses through use of both
keyUp
and performKeyEquivalent
. Then it sends a chooseSearchResult:
message
with the index chosen by the user.
ViewController
listens for this message and, when it is received, uses
AccessibilityWrapper
to try to find a window in the filtered list of windows
whose index matches the number chosen by the user (after being converted into
a zero-indexed number, because the array in which the window descriptions are
stored is zero-indexed).
AccessibilityWrapper
houses a collection of class methods designed to retrieve
and take action on windows currently open on the system in any non-system
and non-menu application on the system. These are all convenience wrappers around
low-level Accessibility API functions.
- No tests
- The window should grow and shrink to match the size of the TableView. Some ideas:
// Resize the view to match the number of results.
func resizeToFitContent() {
let contentHeight = scrollView.documentView!.frame.size.height
let searchBoxHeight = searchField.frame.size.height
let prevScrollHeight = scrollView.frame.size.height
let prevScrollY = scrollView.frame.origin.y
println("beep")
println("view height \(view.frame.size.height)")
println("scrollview height \(scrollView.frame.size.height)")
println("scrollview y \(scrollView.frame.origin.y)")
println("tableview height \(tableView.frame.size.height)")
// This isn't working. :(
// view.frame.size.height = contentHeight + searchBoxHeight
// scrollView.frame.size.height = contentHeight
// if (prevScrollHeight > contentHeight) {
// scrollView.frame.origin.y = prevScrollY - (prevScrollHeight - contentHeight)
// }
// else if (prevScrollHeight < contentHeight) {
// scrollView.frame.origin.y = prevScrollY + (contentHeight - prevScrollHeight)
// }
println("view height \(view.frame.size.height)")
println("scrollview height \(scrollView.frame.size.height)")
println("scrollview y \(scrollView.frame.origin.y)")
println("tableview height \(tableView.frame.size.height)")
}
- Runs as a menu bar app, but you can't quit it or view preferences
- Ideally it would show separate lists of applications, one for each open space, not just the current one