blob: b00e231e0f1c6d1191e947b3933e61ce53991ca2 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert' show json;
import 'package:flutter/material.dart' show VoidCallback;
import 'package:meta/meta.dart';
import 'package:fidl_fuchsia_ui_input2/fidl_async.dart';
import 'package:fidl_fuchsia_ui_shortcut/fidl_async.dart' as ui_shortcut
show Registry, RegistryProxy, Shortcut, Trigger, Listener, ListenerBinding;
import 'package:fidl_fuchsia_ui_views/fidl_async.dart' show ViewRef;
import 'package:fuchsia_services/services.dart' show StartupContext;
import 'package:zircon/zircon.dart' show EventPairPair;
/// Listens for keyboard shortcuts and triggers callbacks when they occur.
class KeyboardShortcuts extends ui_shortcut.Listener {
final ui_shortcut.Registry registry;
final Map<String, VoidCallback> actions;
final List<Shortcut> shortcuts;
// final _viewRef = ViewRef(reference: EventPairPair().first);
final ViewRef _viewRef;
final ui_shortcut.ListenerBinding _listenerBinding;
KeyboardShortcuts({
@required this.registry,
@required this.actions,
@required String bindings,
ui_shortcut.ListenerBinding listenerBinding,
ViewRef viewRef,
}) : shortcuts = _decodeJsonBindings(bindings, actions),
_viewRef = viewRef ?? ViewRef(reference: EventPairPair().first),
_listenerBinding = listenerBinding ?? ui_shortcut.ListenerBinding() {
registry.setView(_viewRef, _listenerBinding.wrap(this));
shortcuts.forEach(registry.registerShortcut);
}
factory KeyboardShortcuts.fromStartupContext(
StartupContext startupContext, {
Map<String, VoidCallback> actions,
String bindings,
}) {
final shortcutRegistry = ui_shortcut.RegistryProxy();
startupContext.incoming.connectToService(shortcutRegistry);
return KeyboardShortcuts(
registry: shortcutRegistry,
actions: actions,
bindings: bindings,
);
}
void dispose() {
if (registry is ui_shortcut.RegistryProxy) {
ui_shortcut.RegistryProxy proxy = registry;
proxy.ctrl.close();
}
shortcuts.clear();
_listenerBinding.close();
}
@override
Future<bool> onShortcut(int id) async {
Shortcut shortcut = shortcuts.firstWhere((shortcut) => shortcut.id == id);
shortcut.onKey();
return shortcut?.exclusive ?? false;
}
/// Returns keyboard binding help text.
String helpText() {
final result = <String, List<String>>{};
for (final binding in shortcuts) {
if (result.containsKey(binding.description)) {
result[binding.description].add(binding.chord);
} else {
result[binding.description] = [binding.chord];
}
}
final buf = StringBuffer();
for (final description in result.keys) {
buf.writeln(description);
for (final chord in result[description]) {
buf
..write(' ')
..writeln(chord);
}
}
return buf.toString();
}
static List<Shortcut> _decodeJsonBindings(
String bindings, Map<String, VoidCallback> actions) {
final data = json.decode(bindings, reviver: (key, value) {
if (actions.containsKey(key)) {
if (value is! List) {
return null;
}
List<dynamic> chords = value;
VoidCallback callback = actions[key];
return chords.whereType<Map>().map((c) {
return Shortcut.fromJSON(
object: c,
onKey: callback,
action: key,
);
}).toList();
}
return value;
});
if (data is! Map) {
return [];
}
Map<String, dynamic> kvData = data
..removeWhere((name, value) =>
!actions.containsKey(name) ||
value == null ||
value is! List ||
value.isEmpty);
return kvData
.map((k, v) => MapEntry<String, List<Shortcut>>(k, v))
.values
.expand((c) => c)
.toList();
}
}
/// Defines a keyboard shortcut binding.
class Shortcut extends ui_shortcut.Shortcut {
static int lastId = 0;
bool exclusive = true;
String action;
String chord;
String description;
VoidCallback onKey;
Shortcut({
Key key,
Modifiers modifiers,
bool usePriority = true,
this.exclusive,
this.onKey,
this.action,
this.description = '',
}) : super(
id: ++lastId,
modifiers: modifiers,
key: key,
usePriority: usePriority);
Shortcut.fromJSON({
Map<String, dynamic> object,
this.onKey,
this.action,
}) : chord = object['chord'],
description = object['description'],
exclusive = object['exclusive'] ?? true,
super(
id: ++lastId,
key: Key.$valueOf(object['char']),
trigger: object['char'] == null && object['modifier'] != null
? ui_shortcut.Trigger.keyPressedAndReleased
: null,
modifiers: _modifiersFromArray(object['modifier']));
@override
bool operator ==(dynamic other) =>
other is Shortcut &&
id == other.id &&
modifiers == other.modifiers &&
key == other.key &&
usePriority == other.usePriority;
@override
int get hashCode =>
id.hashCode ^ modifiers.hashCode ^ key.hashCode ^ usePriority.hashCode;
@override
String toString() =>
'id: $id key: $key modifiers: $modifiers action: $action';
static Modifiers _modifiersFromArray(String s) {
if (s == null) {
return null;
}
return s
.split('+')
.map((x) => x.trim())
.map(_modifierFromString)
.reduce((prev, next) => prev | next);
}
static Modifiers _modifierFromString(String s) {
switch (s) {
case 'shift':
return Modifiers.shift;
case 'leftShift':
return Modifiers.leftShift;
case 'rightShift':
return Modifiers.rightShift;
case 'control':
return Modifiers.control;
case 'leftControl':
return Modifiers.leftControl;
case 'rightControl':
return Modifiers.rightControl;
case 'alt':
return Modifiers.alt;
case 'leftAlt':
return Modifiers.leftAlt;
case 'rightAlt':
return Modifiers.rightAlt;
case 'meta':
return Modifiers.meta;
case 'leftMeta':
return Modifiers.leftMeta;
case 'rightMeta':
return Modifiers.rightMeta;
case 'capsLock':
return Modifiers.capsLock;
case 'numLock':
return Modifiers.numLock;
case 'scrollLock':
return Modifiers.scrollLock;
default:
return Modifiers.$none;
}
}
}