Files

462 lines
23 KiB
Go

package input
import "time"
// Command is a semantic request; native callbacks never execute application work.
type Command string
const (
SelectPreviousSpeechSetting Command = "select-previous-speech-setting"
SelectNextSpeechSetting Command = "select-next-speech-setting"
DecreaseSpeechSetting Command = "decrease-speech-setting"
IncreaseSpeechSetting Command = "increase-speech-setting"
SaveSpeechSettings Command = "save-speech-settings"
PresentTime Command = "present-time"
PresentDate Command = "present-date"
PresentClipboard Command = "present-clipboard"
ShowSpeechHistory Command = "show-speech-history"
ToggleSpeechMonitor Command = "toggle-speech-monitor"
CycleSpeechMode Command = "cycle-speech-mode"
CycleProgressBarOutput Command = "cycle-progress-bar-output"
PresentVersion Command = "present-version"
ToggleLearnMode Command = "toggle-learn-mode"
ShowActions Command = "show-actions"
InternalUIKey Command = "internal-ui-key"
InternalUIDismiss Command = "internal-ui-dismiss"
LearnKey Command = "learn-key"
LearnDocumentation Command = "learn-documentation"
LearnDefaultShortcuts Command = "learn-default-shortcuts"
LearnApplicationShortcuts Command = "learn-application-shortcuts"
StickyFocusMode Command = "sticky-focus-mode"
StickyBrowseMode Command = "sticky-browse-mode"
ToggleDocumentMode Command = "toggle-document-mode"
DocumentNextHeading Command = "document-next-heading"
DocumentPreviousHeading Command = "document-previous-heading"
DocumentNextLink Command = "document-next-link"
DocumentPreviousLink Command = "document-previous-link"
DocumentNextButton Command = "document-next-button"
DocumentPreviousButton Command = "document-previous-button"
DocumentNextCheckBox Command = "document-next-check-box"
DocumentPreviousCheckBox Command = "document-previous-check-box"
DocumentNextList Command = "document-next-list"
DocumentPreviousList Command = "document-previous-list"
DocumentNextSeparator Command = "document-next-separator"
DocumentPreviousSeparator Command = "document-previous-separator"
DocumentNextHeadingAtLevel1 Command = "document-next-heading-at-level-1"
DocumentPreviousHeadingAtLevel1 Command = "document-previous-heading-at-level-1"
DocumentNextHeadingAtLevel2 Command = "document-next-heading-at-level-2"
DocumentPreviousHeadingAtLevel2 Command = "document-previous-heading-at-level-2"
DocumentNextHeadingAtLevel3 Command = "document-next-heading-at-level-3"
DocumentPreviousHeadingAtLevel3 Command = "document-previous-heading-at-level-3"
DocumentNextHeadingAtLevel4 Command = "document-next-heading-at-level-4"
DocumentPreviousHeadingAtLevel4 Command = "document-previous-heading-at-level-4"
DocumentNextHeadingAtLevel5 Command = "document-next-heading-at-level-5"
DocumentPreviousHeadingAtLevel5 Command = "document-previous-heading-at-level-5"
DocumentNextHeadingAtLevel6 Command = "document-next-heading-at-level-6"
DocumentPreviousHeadingAtLevel6 Command = "document-previous-heading-at-level-6"
DocumentNextBlockQuote Command = "document-next-block-quote"
DocumentPreviousBlockQuote Command = "document-previous-block-quote"
DocumentNextComboBox Command = "document-next-combo-box"
DocumentPreviousComboBox Command = "document-previous-combo-box"
DocumentNextEntry Command = "document-next-entry"
DocumentPreviousEntry Command = "document-previous-entry"
DocumentNextFormField Command = "document-next-form-field"
DocumentPreviousFormField Command = "document-previous-form-field"
DocumentNextImage Command = "document-next-image"
DocumentPreviousImage Command = "document-previous-image"
DocumentNextLandmark Command = "document-next-landmark"
DocumentPreviousLandmark Command = "document-previous-landmark"
DocumentNextListItem Command = "document-next-list-item"
DocumentPreviousListItem Command = "document-previous-list-item"
DocumentNextLiveRegion Command = "document-next-live-region"
DocumentPreviousLiveRegion Command = "document-previous-live-region"
DocumentNextParagraph Command = "document-next-paragraph"
DocumentPreviousParagraph Command = "document-previous-paragraph"
DocumentNextRadioButton Command = "document-next-radio-button"
DocumentPreviousRadioButton Command = "document-previous-radio-button"
DocumentNextTable Command = "document-next-table"
DocumentPreviousTable Command = "document-previous-table"
DocumentNextUnvisitedLink Command = "document-next-unvisited-link"
DocumentPreviousUnvisitedLink Command = "document-previous-unvisited-link"
DocumentNextVisitedLink Command = "document-next-visited-link"
DocumentPreviousVisitedLink Command = "document-previous-visited-link"
DocumentNextClickable Command = "document-next-clickable"
DocumentPreviousClickable Command = "document-previous-clickable"
DocumentNextChunk Command = "document-next-chunk"
DocumentPreviousChunk Command = "document-previous-chunk"
DocumentCaretNextCharacter Command = "document-caret-next-character"
DocumentCaretPreviousCharacter Command = "document-caret-previous-character"
DocumentCaretNextWord Command = "document-caret-next-word"
DocumentCaretPreviousWord Command = "document-caret-previous-word"
DocumentCaretNextLine Command = "document-caret-next-line"
DocumentCaretPreviousLine Command = "document-caret-previous-line"
DocumentCaretLineStart Command = "document-caret-line-start"
DocumentCaretLineEnd Command = "document-caret-line-end"
DocumentCaretDocumentStart Command = "document-caret-document-start"
DocumentCaretDocumentEnd Command = "document-caret-document-end"
DocumentTableLeft Command = "document-table-left"
DocumentTableRight Command = "document-table-right"
DocumentTableUp Command = "document-table-up"
DocumentTableDown Command = "document-table-down"
DocumentTableFirst Command = "document-table-first"
DocumentTableLast Command = "document-table-last"
DocumentActivate Command = "document-activate"
ReadAll Command = "read-all"
DisableTentacles Command = "disable-tentacles"
Quit Command = "quit"
Preferences Command = "preferences"
ApplicationPreferences Command = "application-preferences"
ChooseApplicationPreferences Command = "choose-application-preferences"
PresentCellFormula Command = "present-cell-formula"
WhereAmI Command = "where-am-i"
PresentCharacterAttributes Command = "present-character-attributes"
PresentLastNotification Command = "present-last-notification"
PresentPreviousNotification Command = "present-previous-notification"
PresentNextNotification Command = "present-next-notification"
ShowNotificationList Command = "show-notification-list"
ObjectParent Command = "object-parent"
ObjectFirstChild Command = "object-first-child"
ObjectPreviousSibling Command = "object-previous-sibling"
ObjectNextSibling Command = "object-next-sibling"
ReviewWindowStart Command = "review-window-start"
ReviewWindowEnd Command = "review-window-end"
ReviewPreviousLine Command = "review-previous-line"
ReviewCurrentLine Command = "review-current-line"
ReviewNextLine Command = "review-next-line"
ReviewPreviousWord Command = "review-previous-word"
ReviewCurrentWord Command = "review-current-word"
ReviewSpellWord Command = "review-spell-word"
ReviewPhoneticWord Command = "review-phonetic-word"
ReviewNextWord Command = "review-next-word"
ReviewPreviousCharacter Command = "review-previous-character"
ReviewCurrentCharacter Command = "review-current-character"
ReviewPhoneticCharacter Command = "review-phonetic-character"
ReviewNextCharacter Command = "review-next-character"
ReviewWordAbove Command = "review-word-above"
ReviewWordBelow Command = "review-word-below"
ReviewLineEnd Command = "review-line-end"
)
type KeyboardLayout int
const (
DesktopKeyboardLayout KeyboardLayout = 1
LaptopKeyboardLayout KeyboardLayout = 2
)
const (
returnKeySymbol uint32 = 0xff0d
keypadEnterKeySymbol uint32 = 0xff8d
leftKeySymbol uint32 = 0xff51
upKeySymbol uint32 = 0xff52
rightKeySymbol uint32 = 0xff53
downKeySymbol uint32 = 0xff54
leftControlKeySymbol uint32 = 0xffe3
rightControlKeySymbol uint32 = 0xffe4
leftShiftKeySymbol uint32 = 0xffe1
rightShiftKeySymbol uint32 = 0xffe2
controlModifier uint16 = 1 << 2
)
type CommandChord struct {
Symbol uint32
Modifiers uint16
CthulhuModifier bool
}
type CommandBindings map[CommandChord]Command
// CommandEvent retains the initiating X server time across asynchronous delivery.
type CommandEvent struct {
Command Command
Timestamp uint32
Chord CommandChord
KeyName string
LearnModeStarted bool
UISession uint64
Text string
}
type LockState struct {
Name string
Locked bool
Timestamp uint32
}
func (state LockState) Text() string {
if state.Locked {
return state.Name + " on"
}
return state.Name + " off"
}
func DefaultCommandBindings(layout KeyboardLayout) CommandBindings {
bindings := CommandBindings{
{Symbol: 'a', Modifiers: 1, CthulhuModifier: true}: ShowActions,
{Symbol: leftKeySymbol, CthulhuModifier: true}: SelectPreviousSpeechSetting,
{Symbol: rightKeySymbol, CthulhuModifier: true}: SelectNextSpeechSetting,
{Symbol: downKeySymbol, CthulhuModifier: true}: DecreaseSpeechSetting,
{Symbol: upKeySymbol, CthulhuModifier: true}: IncreaseSpeechSetting,
{Symbol: 's', Modifiers: controlModifier | 1, CthulhuModifier: true}: SaveSpeechSettings,
{Symbol: 't', CthulhuModifier: true}: PresentTime,
{Symbol: 'c', Modifiers: 1, CthulhuModifier: true}: PresentClipboard,
{Symbol: 'h', Modifiers: controlModifier, CthulhuModifier: true}: ShowSpeechHistory,
{Symbol: 'd', Modifiers: 1, CthulhuModifier: true}: ToggleSpeechMonitor,
{Symbol: 's', CthulhuModifier: true}: CycleSpeechMode,
{Symbol: 'u', CthulhuModifier: true}: CycleProgressBarOutput,
{Symbol: 'v', Modifiers: 1, CthulhuModifier: true}: PresentVersion,
{Symbol: 'a', CthulhuModifier: true}: ToggleDocumentMode,
{Symbol: 'h', CthulhuModifier: true}: ToggleLearnMode,
{Symbol: 0xffbe, CthulhuModifier: true}: ToggleLearnMode,
{Symbol: 'h', Modifiers: 1, CthulhuModifier: true}: DocumentPreviousHeading,
{Symbol: 'k', CthulhuModifier: true, Modifiers: controlModifier}: DocumentNextLink,
{Symbol: 'k', Modifiers: controlModifier | 1, CthulhuModifier: true}: DocumentPreviousLink,
{Symbol: 'q', CthulhuModifier: true}: Quit,
{Symbol: 'q', Modifiers: 5, CthulhuModifier: true}: DisableTentacles,
{Symbol: 'n', CthulhuModifier: true}: PresentLastNotification,
{Symbol: 'n', Modifiers: controlModifier, CthulhuModifier: true}: ShowNotificationList,
{Symbol: ' ', CthulhuModifier: true}: Preferences,
{Symbol: ' ', Modifiers: controlModifier, CthulhuModifier: true}: ApplicationPreferences,
{Symbol: ' ', Modifiers: 1, CthulhuModifier: true}: ApplicationPreferences,
{Symbol: ' ', Modifiers: controlModifier | 1, CthulhuModifier: true}: ChooseApplicationPreferences,
{Symbol: 'f', CthulhuModifier: true}: PresentCharacterAttributes,
{Symbol: upKeySymbol, Modifiers: controlModifier, CthulhuModifier: true}: ObjectParent,
{Symbol: downKeySymbol, Modifiers: controlModifier, CthulhuModifier: true}: ObjectFirstChild,
{Symbol: leftKeySymbol, Modifiers: controlModifier, CthulhuModifier: true}: ObjectPreviousSibling,
{Symbol: rightKeySymbol, Modifiers: controlModifier, CthulhuModifier: true}: ObjectNextSibling,
}
if layout == DesktopKeyboardLayout {
bindings[CommandChord{Symbol: 'o', CthulhuModifier: true}] = OCRWindow
bindings[CommandChord{Symbol: 0xffab}] = ReadAll // KP_Add
bindings[CommandChord{Symbol: keypadEnterKeySymbol}] = WhereAmI
bindings[CommandChord{Symbol: '=', CthulhuModifier: true}] = PresentCellFormula
bindings[CommandChord{Symbol: 0xff95, CthulhuModifier: true}] = ReviewWindowStart
bindings[CommandChord{Symbol: 0xff9a, CthulhuModifier: true}] = ReviewWindowEnd
bindings[CommandChord{Symbol: 0xff96, CthulhuModifier: true}] = ReviewWordAbove
bindings[CommandChord{Symbol: 0xff98, CthulhuModifier: true}] = ReviewWordBelow
bindings[CommandChord{Symbol: 0xff9c, CthulhuModifier: true}] = ReviewLineEnd
} else if layout == LaptopKeyboardLayout {
bindings[CommandChord{Symbol: 'o', Modifiers: 1, CthulhuModifier: true}] = OCRWindow
bindings[CommandChord{Symbol: 'u', Modifiers: 1, CthulhuModifier: true}] = CycleProgressBarOutput
bindings[CommandChord{Symbol: ';', CthulhuModifier: true}] = ReadAll
bindings[CommandChord{Symbol: returnKeySymbol, CthulhuModifier: true}] = WhereAmI
bindings[CommandChord{Symbol: 'u', Modifiers: controlModifier, CthulhuModifier: true}] = ReviewWindowStart
bindings[CommandChord{Symbol: 'o', Modifiers: controlModifier, CthulhuModifier: true}] = ReviewWindowEnd
bindings[CommandChord{Symbol: 'j', Modifiers: controlModifier, CthulhuModifier: true}] = ReviewWordAbove
bindings[CommandChord{Symbol: 'l', Modifiers: controlModifier, CthulhuModifier: true}] = ReviewWordBelow
bindings[CommandChord{Symbol: 'm', Modifiers: controlModifier, CthulhuModifier: true}] = ReviewLineEnd
}
for _, binding := range []struct {
desktop, laptop uint32
command Command
}{
{0xff95, 'u', ReviewPreviousLine}, {0xff97, 'i', ReviewCurrentLine}, {0xff9a, 'o', ReviewNextLine},
{0xff96, 'j', ReviewPreviousWord}, {0xff9d, 'k', ReviewCurrentWord}, {0xff98, 'l', ReviewNextWord},
{0xff9c, 'm', ReviewPreviousCharacter}, {0xff99, ',', ReviewCurrentCharacter}, {0xff9b, '.', ReviewNextCharacter},
} {
if layout == DesktopKeyboardLayout {
bindings[CommandChord{Symbol: binding.desktop}] = binding.command
}
if layout == LaptopKeyboardLayout {
bindings[CommandChord{Symbol: binding.laptop, CthulhuModifier: true}] = binding.command
}
}
return bindings
}
// CommandRouter owns the pressed state of screen-reader modifiers and consumed
// command keys. Commands are emitted on press; native owners retain the grab
// independently until the chord releases, including during handler shutdown.
type CommandRouter struct {
now func() time.Time
lastTimePress time.Time
lastModePress time.Time
modePresses int
lastReviewPress time.Time
lastReviewChord CommandChord
reviewPresses int
modifiers map[uint32]bool
bindings CommandBindings
down map[uint16]bool
consumed map[uint16]bool
}
func NewCommandRouter(modifiers []uint32, bindings CommandBindings) *CommandRouter {
router := &CommandRouter{
now: time.Now,
modifiers: make(map[uint32]bool),
bindings: make(CommandBindings, len(bindings)),
down: make(map[uint16]bool),
consumed: make(map[uint16]bool),
}
for _, symbol := range modifiers {
router.modifiers[symbol] = true
}
for chord, command := range bindings {
router.bindings[chord] = command
}
return router
}
func (router *CommandRouter) Handle(symbol uint32, code uint16, modifiers uint16, pressed bool) (bool, Command) {
consume := false
if !pressed {
consume = router.down[code] || router.consumed[code]
delete(router.down, code)
delete(router.consumed, code)
} else if router.modifiers[symbol] {
router.down[code] = true
consume = true
} else if router.consumed[code] {
consume = true
} else {
chord := CommandChord{
Symbol: symbol,
Modifiers: modifiers &^ uint16(2|16),
CthulhuModifier: len(router.down) != 0,
}
if command := router.bindings[chord]; command != "" {
router.consumed[code] = true
command = router.review_press(command, chord)
if command == ToggleDocumentMode {
now := router.now()
if !router.lastModePress.IsZero() && now.Sub(router.lastModePress) >= 0 && now.Sub(router.lastModePress) <= 500*time.Millisecond {
router.modePresses++
} else {
router.modePresses = 1
}
router.lastModePress = now
switch router.modePresses {
case 2:
command = StickyFocusMode
case 3:
command = StickyBrowseMode
router.lastModePress = time.Time{}
}
} else {
router.lastModePress = time.Time{}
}
if command == PresentTime {
now := router.now()
if !router.lastTimePress.IsZero() && now.Sub(router.lastTimePress) >= 0 && now.Sub(router.lastTimePress) <= 500*time.Millisecond {
command = PresentDate
router.lastTimePress = time.Time{}
} else {
router.lastTimePress = now
}
} else {
router.lastTimePress = time.Time{}
}
return true, command
}
router.lastTimePress = time.Time{}
router.lastModePress = time.Time{}
router.lastReviewPress = time.Time{}
}
return consume, ""
}
func (router *CommandRouter) IsModifier(symbol uint32) bool {
switch symbol {
case leftControlKeySymbol, rightControlKeySymbol, leftShiftKeySymbol, rightShiftKeySymbol,
0xffe7, 0xffe8, 0xffe9, 0xffea, 0xffeb, 0xffec, 0xffed, 0xffee: // Meta, Alt, Super, Hyper.
return true
}
return router.modifiers[symbol]
}
// Chord reports the keys currently held before handling the next press.
func (router *CommandRouter) Chord(symbol uint32, modifiers uint16) CommandChord {
return CommandChord{Symbol: symbol, Modifiers: modifiers &^ uint16(2|16), CthulhuModifier: len(router.down) != 0}
}
func (router *CommandRouter) Reset() {
clear(router.down)
clear(router.consumed)
}
// Browse bindings are installed only while the runtime owns live web content.
func BrowseCommandBindings(layout KeyboardLayout) CommandBindings {
bindings := DefaultCommandBindings(layout)
caretBindings := CommandBindings{
{Symbol: leftKeySymbol, Modifiers: 1 | 8}: DocumentTableLeft,
{Symbol: rightKeySymbol, Modifiers: 1 | 8}: DocumentTableRight,
{Symbol: upKeySymbol, Modifiers: 1 | 8}: DocumentTableUp,
{Symbol: downKeySymbol, Modifiers: 1 | 8}: DocumentTableDown,
{Symbol: 0xff50, Modifiers: 1 | 8}: DocumentTableFirst,
{Symbol: 0xff57, Modifiers: 1 | 8}: DocumentTableLast,
{Symbol: rightKeySymbol}: DocumentCaretNextCharacter,
{Symbol: leftKeySymbol}: DocumentCaretPreviousCharacter,
{Symbol: rightKeySymbol, Modifiers: controlModifier}: DocumentCaretNextWord,
{Symbol: leftKeySymbol, Modifiers: controlModifier}: DocumentCaretPreviousWord,
{Symbol: downKeySymbol}: DocumentCaretNextLine,
{Symbol: upKeySymbol}: DocumentCaretPreviousLine,
{Symbol: 0xff50}: DocumentCaretLineStart,
{Symbol: 0xff57}: DocumentCaretLineEnd,
{Symbol: 0xff50, Modifiers: controlModifier}: DocumentCaretDocumentStart,
{Symbol: 0xff57, Modifiers: controlModifier}: DocumentCaretDocumentEnd,
{Symbol: returnKeySymbol}: DocumentActivate,
}
for chord, command := range caretBindings {
bindings[chord] = command
}
documentBindings := []struct {
symbol uint32
next Command
previous Command
}{
{'h', DocumentNextHeading, DocumentPreviousHeading},
{'k', DocumentNextLink, DocumentPreviousLink},
{'b', DocumentNextButton, DocumentPreviousButton},
{'x', DocumentNextCheckBox, DocumentPreviousCheckBox},
{'l', DocumentNextList, DocumentPreviousList},
{'s', DocumentNextSeparator, DocumentPreviousSeparator},
{'q', DocumentNextBlockQuote, DocumentPreviousBlockQuote},
{'c', DocumentNextComboBox, DocumentPreviousComboBox},
{'e', DocumentNextEntry, DocumentPreviousEntry},
{'f', DocumentNextFormField, DocumentPreviousFormField},
{'g', DocumentNextImage, DocumentPreviousImage},
{'m', DocumentNextLandmark, DocumentPreviousLandmark},
{'i', DocumentNextListItem, DocumentPreviousListItem},
{'d', DocumentNextLiveRegion, DocumentPreviousLiveRegion},
{'p', DocumentNextParagraph, DocumentPreviousParagraph},
{'r', DocumentNextRadioButton, DocumentPreviousRadioButton},
{'t', DocumentNextTable, DocumentPreviousTable},
{'u', DocumentNextUnvisitedLink, DocumentPreviousUnvisitedLink},
{'v', DocumentNextVisitedLink, DocumentPreviousVisitedLink},
{'a', DocumentNextClickable, DocumentPreviousClickable},
{'o', DocumentNextChunk, DocumentPreviousChunk},
}
for _, binding := range documentBindings {
bindings[CommandChord{Symbol: binding.symbol}] = binding.next
bindings[CommandChord{Symbol: binding.symbol, Modifiers: 1}] = binding.previous
}
headingLevels := []struct {
level int
next Command
previous Command
}{
{1, DocumentNextHeadingAtLevel1, DocumentPreviousHeadingAtLevel1},
{2, DocumentNextHeadingAtLevel2, DocumentPreviousHeadingAtLevel2},
{3, DocumentNextHeadingAtLevel3, DocumentPreviousHeadingAtLevel3},
{4, DocumentNextHeadingAtLevel4, DocumentPreviousHeadingAtLevel4},
{5, DocumentNextHeadingAtLevel5, DocumentPreviousHeadingAtLevel5},
{6, DocumentNextHeadingAtLevel6, DocumentPreviousHeadingAtLevel6},
}
for _, binding := range headingLevels {
symbol := uint32('0' + binding.level)
bindings[CommandChord{Symbol: symbol}] = binding.next
bindings[CommandChord{Symbol: symbol, Modifiers: 1}] = binding.previous
}
return bindings
}
// ReplaceBindings preserves multi-press timing across browse grab changes.
// Native owners call this only after every consumed key has been released.
func (router *CommandRouter) ReplaceBindings(bindings CommandBindings) {
router.bindings = make(CommandBindings, len(bindings))
for chord, command := range bindings {
router.bindings[chord] = command
}
}