AccessibilityUIExamples/Stepper/CustomStepperView.swift
/* |
See LICENSE folder for this sample’s licensing information. |
Abstract: |
An example demonstrating adding accessibility to custom MTKView subclass that behaves |
like a stepper by implementing the NSAccessibilityStepper protocol. |
*/ |
import Cocoa |
import MetalKit |
/* |
IMPORTANT: This is not a template for developing a custom stepper. |
This sample is intended to demonstrate how to add accessibility to |
existing custom controls that are not implemented using the preferred methods. |
For information on how to create custom controls please visit http://developer.apple.com |
*/ |
class CustomStepperView: MTKView, NSAccessibilityStepper { |
// MARK: - Internals |
fileprivate struct LayoutInfo { |
static let stepperMinValue = CGFloat(0.0) |
static let stepperMaxValue = CGFloat(100.0) |
static let stepperStepSize = CGFloat(5.0) |
} |
var upButtonMouseDown = false |
var upButtonShowDepressed = false |
var downButtonMouseDown = false |
var downButtonShowDepressed = false |
var value = CGFloat(0.0) { |
didSet { |
if value < minValue { |
value = minValue |
} |
if value > maxValue { |
value = maxValue |
} |
needsDisplay = true |
} |
} |
var minValue = CGFloat(0.0) |
var maxValue = CGFloat(0.0) |
var stepSize = CGFloat(0.0) |
// MARK: - Metal Internals |
enum BufferIndices: Int { |
case vertices = 0 |
case viewportSize |
case color |
} |
var trianglePipelineState: MTLRenderPipelineState? |
var backgroundPipelineState: MTLRenderPipelineState? |
var texture: MTLTexture? |
var commandQueue: MTLCommandQueue? |
var viewportSize: vector_uint2 = vector_uint2(0, 0) |
// MARK: - View Lifecycle |
required override init(frame frameRect: CGRect, device: MTLDevice?) { |
super.init(frame: frameRect, device: device) |
commonInit() |
} |
required init(coder: NSCoder) { |
super.init(coder: coder) |
commonInit() |
} |
fileprivate func commonInit() { |
loadMetal() |
minValue = LayoutInfo.stepperMinValue |
maxValue = LayoutInfo.stepperMaxValue |
stepSize = LayoutInfo.stepperStepSize |
// Register for mouse events that affect drawing. |
let trackingArea = NSTrackingArea(rect: bounds, |
options: [NSTrackingArea.Options.activeWhenFirstResponder, NSTrackingArea.Options.mouseEnteredAndExited], |
owner: self, |
userInfo: nil) |
addTrackingArea(trackingArea) |
} |
// MARK: - Events |
// Set to allow keyDown to be called. |
override var acceptsFirstResponder: Bool { return true } |
var actionHandler: (() -> Void)? |
fileprivate func performAfterDelay(delay: Double, onCompletion: @escaping() -> Void) { |
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delay, execute: { |
onCompletion() |
}) |
} |
fileprivate func performIncrementButtonPress() { |
upButtonShowDepressed = true |
downButtonShowDepressed = false |
needsDisplay = true |
let delayInSeconds = 0.1 |
performAfterDelay(delay: delayInSeconds) { |
self.upButtonShowDepressed = false |
self.downButtonShowDepressed = false |
self.needsDisplay = true |
self.increment() |
} |
} |
fileprivate func performDecrementButtonPress() { |
upButtonShowDepressed = false |
downButtonShowDepressed = true |
needsDisplay = true |
let delayInSeconds = 0.1 |
performAfterDelay(delay: delayInSeconds) { |
self.upButtonShowDepressed = false |
self.downButtonShowDepressed = false |
self.needsDisplay = true |
self.decrement() |
} |
} |
fileprivate func increment() { |
value += stepSize |
actionHandler!() |
} |
fileprivate func decrement() { |
value -= stepSize |
actionHandler!() |
} |
// MARK: - Layout |
fileprivate func upButtonRect() -> NSRect { |
return NSRect(x: bounds.origin.x, y: bounds.midY, width: bounds.size.width, height: bounds.size.height / 2.0) |
} |
fileprivate func downButtonRect() -> NSRect { |
return NSRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.size.width, height: bounds.size.height / 2.0) |
} |
// MARK: - Mouse Events |
override func mouseDown(with event: NSEvent) { |
super.mouseDown(with: event) |
let mouseDownPoint = convert(event.locationInWindow, from: nil) |
if upButtonRect().contains(mouseDownPoint) { |
upButtonMouseDown = true |
upButtonShowDepressed = true |
downButtonMouseDown = false |
downButtonShowDepressed = false |
} else if downButtonRect().contains(mouseDownPoint) { |
upButtonMouseDown = false |
upButtonShowDepressed = false |
downButtonMouseDown = true |
downButtonShowDepressed = true |
} |
needsDisplay = true |
} |
override func mouseUp(with event: NSEvent) { |
super.mouseUp(with: event) |
let mouseUpPoint = convert(event.locationInWindow, from: nil) |
if upButtonMouseDown && |
upButtonShowDepressed && |
upButtonRect().contains(mouseUpPoint) { |
increment() |
} else if downButtonMouseDown && |
downButtonShowDepressed && |
downButtonRect().contains(mouseUpPoint) { |
decrement() |
} |
upButtonMouseDown = false |
upButtonShowDepressed = false |
downButtonMouseDown = false |
downButtonShowDepressed = false |
needsDisplay = true |
} |
override func mouseEntered(with event: NSEvent) { |
super.mouseEntered(with: event) |
let mousePoint = convert(event.locationInWindow, from:nil) |
if upButtonRect().contains(mousePoint) { |
upButtonShowDepressed = upButtonMouseDown |
} else if downButtonRect().contains(mousePoint) { |
downButtonShowDepressed = downButtonMouseDown |
} |
needsDisplay = true |
} |
override func mouseExited(with event: NSEvent) { |
super.mouseExited(with: event) |
upButtonShowDepressed = false |
downButtonShowDepressed = false |
needsDisplay = true |
} |
// MARK: - Keyboard Events |
override func keyDown(with event: NSEvent) { |
// Increment value on spacebar. |
if event.characters == " " { |
performIncrementButtonPress() |
} else { |
// Arrow keys are associated with the numeric keypad |
if event.modifierFlags.contains(.numericPad) { |
interpretKeyEvents([event]) |
} else { |
super.keyDown(with: event) |
} |
} |
} |
override func moveUp(_ sender: Any?) { |
performIncrementButtonPress() |
} |
override func moveDown(_ sender: Any?) { |
performDecrementButtonPress() |
} |
// MARK: - MetalKit |
func loadMetal() { |
device = MTLCreateSystemDefaultDevice() |
do { |
// Indicate we would like to use the RGBAPixel format. |
colorPixelFormat = MTLPixelFormat.bgra8Unorm_srgb |
// Load all the shader files with a metal file extension in the project. |
let defaultLibrary = device!.makeDefaultLibrary() |
// Load the vertex function from the library. |
let vertexFunction = defaultLibrary!.makeFunction(name: "vertexShader")! |
// Set up a descriptor for creating a pipeline state object. |
let pipelineStateDescriptor = MTLRenderPipelineDescriptor() |
pipelineStateDescriptor.label = "colored triangle pipeline" |
pipelineStateDescriptor.vertexFunction = vertexFunction |
pipelineStateDescriptor.fragmentFunction = defaultLibrary!.makeFunction(name: "triangleFragmentShader") |
pipelineStateDescriptor.colorAttachments[0].pixelFormat = super.colorPixelFormat |
try trianglePipelineState = device!.makeRenderPipelineState(descriptor: pipelineStateDescriptor) |
pipelineStateDescriptor.label = "background quad pipeline" |
pipelineStateDescriptor.fragmentFunction = defaultLibrary!.makeFunction(name: "backgroundFragmentShader") |
try backgroundPipelineState = device!.makeRenderPipelineState(descriptor: pipelineStateDescriptor) |
let textureLoader = MTKTextureLoader(device: device!) |
try texture = textureLoader.newTexture(name: "Background", scaleFactor: 1.0, bundle: nil, options: nil) |
// Create the command queue. |
commandQueue = device?.makeCommandQueue() |
} catch { |
fatalError("Failed to setup Metal.") |
} |
} |
// MARK: - Drawing |
override func drawFocusRingMask() { |
bounds.fill() |
} |
override var focusRingMaskBounds: NSRect { |
return bounds |
} |
override func draw(_ dirtyRect: NSRect) { |
var whiteColor = vector_float4(1.0, 1.0, 1.0, 1.0) |
var pinkDiffusedColor = vector_float4(1.0, 0.0, 1.0, 1.0) |
var yellowDiffusedColor = vector_float4(1.0, 1.0, 0.0, 1.0) |
var halfViewportSize = vector_float2(Float(drawableSize.width) / 2.0, |
Float(drawableSize.height) / 2.0) |
var quadVertices: [Vertex] = [ |
// Pixel Positions |
Vertex(position: vector_float4(x: -halfViewportSize.x, y: halfViewportSize.y, z: 0, w: 1), |
// TexCoords |
texcoord: vector_float4(x: 0, y: 1, z: 0, w: 1)), |
Vertex(position: vector_float4(x: halfViewportSize.x, y: halfViewportSize.y, z: 0, w: 1), |
texcoord: vector_float4(x: 1, y: 1, z: 0, w: 1)), |
Vertex(position: vector_float4(x: halfViewportSize.x, y: -halfViewportSize.y, z: 0, w: 1), |
texcoord: vector_float4(x: 1, y: 0, z: 0, w: 1)), |
Vertex(position: vector_float4(x: halfViewportSize.x, y: -halfViewportSize.y, z: 0, w: 1), |
texcoord: vector_float4(x: 1, y: 0, z: 0, w: 1)), |
Vertex(position: vector_float4(x: -halfViewportSize.x, y: -halfViewportSize.y, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)), |
Vertex(position: vector_float4(x: -halfViewportSize.x, y: halfViewportSize.y, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 1, z: 0, w: 1)) |
] |
var outlineVertices: [Vertex] = [ |
// Note, value of texcoords doesn't matter as the pipeline these triangles are paired with don't |
// sample from a texture. (But we need to fill them in since the vertex shader expects them). |
// Top Triangle (Volume Up) |
Vertex(position: vector_float4(x: -halfViewportSize.x + 6, y: 4, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)), |
Vertex(position: vector_float4(x: 0, y: halfViewportSize.y - 3, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)), |
Vertex(position: vector_float4(x: halfViewportSize.x - 6, y: 4, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)), |
// Bottom Triangle (Volume Down). |
Vertex(position: vector_float4(x: -halfViewportSize.x + 6, y: -4, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)), |
Vertex(position: vector_float4(x: halfViewportSize.x - 6, y: -4, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)), |
Vertex(position: vector_float4(x: 0, y: -halfViewportSize.y + 3, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)) |
] |
var foregroundVertices: [Vertex] = [ |
// Note, value of texcoords doesn't matter as the pipeline these triangles are paired with don't |
// sample from a texture. (But we need to fill them in since the vertex shader expects them). |
// Top Triangle (Volume Up). |
Vertex(position: vector_float4(x: -halfViewportSize.x + 9, y: 6, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)), |
Vertex(position: vector_float4(x: 0, y: halfViewportSize.y - 6, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)), |
Vertex(position: vector_float4(x: halfViewportSize.x - 9, y: 6, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)), |
// Bottom Triangle (Volume Down). |
Vertex(position: vector_float4(x: -halfViewportSize.x + 9, y: -6, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)), |
Vertex(position: vector_float4(x: halfViewportSize.x - 9, y: -6, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)), |
Vertex(position: vector_float4(x: 0, y: -halfViewportSize.y + 6, z: 0, w: 1), |
texcoord: vector_float4(x: 0, y: 0, z: 0, w: 1)) |
] |
// Create a new command buffer for each renderpass to the current drawable. |
let commandBuffer = commandQueue?.makeCommandBuffer() |
commandBuffer?.label = "MyCommand" |
// Obtain a renderPassDescriptor generated from the view's drawable textures. |
let renderPassDescriptor = currentRenderPassDescriptor |
if let renderPassDescriptor = renderPassDescriptor { |
// Create a render command encoder so we can render into something. |
let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor) |
renderEncoder?.label = "MyRenderEncoder" |
renderEncoder?.setCullMode(.back) |
renderEncoder?.setRenderPipelineState(backgroundPipelineState!) |
renderEncoder?.setVertexBytes(quadVertices, |
length: MemoryLayout.size(ofValue: quadVertices[0]) * quadVertices.count, |
index: Int(BufferIndexVertices.rawValue)) |
renderEncoder?.setVertexBytes(&halfViewportSize, |
length: MemoryLayout.size(ofValue: halfViewportSize), |
index: Int(BufferIndexViewportSize.rawValue)) |
renderEncoder?.setFragmentTexture(texture, index: 0) |
renderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) |
renderEncoder?.setRenderPipelineState(trianglePipelineState!) |
renderEncoder?.setVertexBytes(outlineVertices, |
length: MemoryLayout.size(ofValue: outlineVertices[0]) * outlineVertices.count, |
index: BufferIndices.vertices.rawValue) |
renderEncoder?.setFragmentBytes(&whiteColor, |
length: MemoryLayout<vector_float4>.size, |
index: BufferIndices.color.rawValue) |
// Draw or 2 triangles. |
renderEncoder?.drawPrimitives(type: .triangle, vertexStart:0, vertexCount:6) |
renderEncoder?.setVertexBytes(foregroundVertices, |
length: MemoryLayout.size(ofValue: foregroundVertices[0]) * foregroundVertices.count, |
index: BufferIndices.vertices.rawValue) |
// Draw upper triangle. |
if upButtonShowDepressed { |
renderEncoder?.setFragmentBytes(&pinkDiffusedColor, |
length: MemoryLayout.size(ofValue: pinkDiffusedColor), |
index: BufferIndices.color.rawValue) |
} else { |
renderEncoder?.setFragmentBytes(&yellowDiffusedColor, |
length: MemoryLayout.size(ofValue: yellowDiffusedColor), |
index: BufferIndices.color.rawValue) |
} |
renderEncoder?.drawPrimitives(type: .triangle, vertexStart:0, vertexCount:3) |
// Draw lower triangle. |
if downButtonShowDepressed { |
renderEncoder?.setFragmentBytes(&pinkDiffusedColor, |
length: MemoryLayout.size(ofValue: pinkDiffusedColor), |
index: BufferIndices.color.rawValue) |
} else { |
renderEncoder?.setFragmentBytes(&yellowDiffusedColor, |
length: MemoryLayout.size(ofValue: yellowDiffusedColor), |
index: BufferIndices.color.rawValue) |
} |
renderEncoder?.drawPrimitives(type: .triangle, vertexStart: 3, vertexCount: 3) |
renderEncoder?.endEncoding() |
if let drawable = currentDrawable { |
commandBuffer?.present(drawable) |
} |
} |
commandBuffer?.commit() |
} |
} |
// MARK: - |
extension CustomStepperView { |
// MARK: NSAccessibilityStepper |
override func accessibilityLabel() -> String? { |
return NSLocalizedString("Volume", comment: "accessibility label for the volume stepper") |
} |
override func accessibilityHelp() -> String? { |
return NSLocalizedString("Adjusts the volume", comment: "accessibility help for the volume stepper") |
} |
override func accessibilityPerformIncrement() -> Bool { |
performIncrementButtonPress() |
return true |
} |
override func accessibilityPerformDecrement() -> Bool { |
performDecrementButtonPress() |
return true |
} |
} |
Copyright © 2017 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2017-09-12