Skip to content

Commit

Permalink
feat: support voiceover + "speak items under the cursor" (closes #1070)
Browse files Browse the repository at this point in the history
  • Loading branch information
lwouis committed Aug 30, 2021
1 parent c18aa4d commit c7911f3
Show file tree
Hide file tree
Showing 5 changed files with 26 additions and 2 deletions.
9 changes: 9 additions & 0 deletions src/logic/Windows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ class Windows {
let focusedView = ThumbnailsView.recycledViews[focusedWindowIndex]
focusedView.highlight(true)
App.app.thumbnailsPanel.thumbnailsView.scrollView.contentView.scrollToVisible(focusedView.frame)
voiceOverFocusedWindow()
}

static func voiceOverFocusedWindow() {
guard App.app.appIsBeingUsed && App.app.thumbnailsPanel.isKeyWindow else { return }
let window = ThumbnailsView.recycledViews[focusedWindowIndex]
if window.window_ != nil && window.window != nil {
App.app.thumbnailsPanel.makeFirstResponder(window)
}
}

static func focusedWindow() -> Window? {
Expand Down
1 change: 1 addition & 0 deletions src/ui/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ class App: AppCenterApplication, NSApplicationDelegate {
thumbnailsPanel.thumbnailsView.updateItemsAndLayout(currentScreen)
guard appIsBeingUsed else { return }
thumbnailsPanel.setContentSize(thumbnailsPanel.thumbnailsView.frame.size)
Windows.voiceOverFocusedWindow() // at this point ThumbnailViews are assigned to the window, and ready
thumbnailsPanel.display()
guard appIsBeingUsed else { return }
currentScreen.repositionPanel(thumbnailsPanel, .appleCentered)
Expand Down
12 changes: 12 additions & 0 deletions src/ui/main-window/ThumbnailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ class ThumbnailView: NSStackView {
var isShowingWindowControls = false
var windowlessIcon = FontIcon(.newWindow)

// for VoiceOver cursor
override var canBecomeKeyView: Bool { true }
override var acceptsFirstResponder: Bool { true }

override func isAccessibilityElement() -> Bool { true }

convenience init() {
self.init(frame: .zero)
setupView()
Expand All @@ -44,6 +50,7 @@ class ThumbnailView: NSStackView {
addWindowControls()
addDockLabelIcon()
thumbnail.addSubview(windowlessIcon, positioned: .above, relativeTo: nil)
setAccessibilityChildren([])
}

private func addDockLabelIcon() {
Expand Down Expand Up @@ -109,6 +116,7 @@ class ThumbnailView: NSStackView {
let thumbnailSize = NSSize(width: thumbnailWidth.rounded(), height: thumbnailHeight.rounded())
thumbnail.image?.size = thumbnailSize
thumbnail.frame.size = thumbnailSize
thumbnail.setAccessibilityLabel(element.title)
}
assignIfDifferent(&spacing, Preferences.hideThumbnails ? 0 : Preferences.intraCellPadding)
assignIfDifferent(&hStackView.spacing, Preferences.fontHeight == 0 ? 0 : Preferences.intraCellPadding)
Expand All @@ -117,12 +125,14 @@ class ThumbnailView: NSStackView {
let appIconSize = NSSize(width: Preferences.iconSize, height: Preferences.iconSize)
appIcon.image?.size = appIconSize
appIcon.frame.size = appIconSize
appIcon.setAccessibilityTitle(element.application.runningApplication.localizedName)
}
let labelChanged = label.string != element.title
if labelChanged {
label.string = element.title
// workaround: setting string on NSTextView changes the font (most likely a Cocoa bug)
label.font = Preferences.font
setAccessibilityTitle(element.title)
}
assignIfDifferent(&hiddenIcon.isHidden, !element.isHidden || Preferences.hideStatusIcons)
assignIfDifferent(&fullscreenIcon.isHidden, !element.isFullscreen || Preferences.hideStatusIcons)
Expand Down Expand Up @@ -169,8 +179,10 @@ class ThumbnailView: NSStackView {
let view = dockLabelIcon.subviews[1] as! ThumbnailFontIconView
if dockLabel > 30 {
view.setFilledStar()
view.setAccessibilityLabel("Red badge with star")
} else {
view.setNumber(dockLabel, true)
view.setAccessibilityLabel("Red badge with number \(dockLabel)")
}
dockLabelIcon.setFrameOrigin(NSPoint(x: appIcon.frame.maxX - dockLabelIcon.fittingSize.width - 1, y: appIcon.frame.maxY - dockLabelIcon.fittingSize.height + 4))
}
Expand Down
4 changes: 2 additions & 2 deletions src/ui/main-window/ThumbnailsPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ class ThumbnailsPanel: NSPanel, NSWindowDelegate {
titleVisibility = .hidden
backgroundColor = .clear
contentView!.addSubview(thumbnailsView)
preservesContentDuringLiveResize = false
disableSnapshotRestoration()
// triggering AltTab before or during Space transition animation brings the window on the Space post-transition
collectionBehavior = .canJoinAllSpaces
// 2nd highest level possible; this allows the app to go on top of context menus
// highest level is .screenSaver but makes drag and drop on top the main window impossible
level = .popUpMenu
// helps filter out this window from the thumbnails
setAccessibilitySubrole(.unknown)
// for VoiceOver
setAccessibilityLabel(App.name)
}

func windowDidResignKey(_ notification: Notification) {
Expand Down
2 changes: 2 additions & 0 deletions src/ui/main-window/WindowControlView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class WindowControlView: NSImageView {
originalImage = image
hoveredImage = image.tinted(.init(white: 0, alpha: 0.25))
hovered(false)
setAccessibilityLabel(imageName)
setAccessibilityRole(.button)
}

func hovered(_ isHovered: Bool) {
Expand Down

0 comments on commit c7911f3

Please sign in to comment.