blob: 775108d839c4a8f0ad87b8610c6231dfc9c06b43 [file] [log] [blame]
// Copyright 2018 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:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'
show RawKeyDownEvent, RawKeyEventDataFuchsia;
import 'package:fidl/fidl.dart' show InterfaceRequest;
import 'package:fidl_fuchsia_modular/fidl_async.dart'
show
Interaction,
InteractionType,
QueryListener,
QueryListenerBinding,
Suggestion,
SuggestionProviderProxy,
UserInput;
import 'package:fidl_fuchsia_shell_ermine/fidl_async.dart'
show AskBar, AskBarBinding;
import 'package:fuchsia_services/services.dart' show StartupContext;
import 'package:lib.widgets/model.dart' show SpringModel;
import 'package:zircon/zircon.dart' show Vmo;
const int _kMaxSuggestions = 20;
// Keyboard HID usage values defined in:
// https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf
const int _kUpArrow = 82;
const int _kDownArrow = 81;
const int _kPageDown = 78;
const int _kPageUp = 75;
const int _kEsc = 41;
class AskModel extends ChangeNotifier {
final StartupContext startupContext;
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
final ValueNotifier<bool> visibility = ValueNotifier(false);
final ValueNotifier<List<Suggestion>> suggestions =
ValueNotifier(<Suggestion>[]);
final ValueNotifier<int> selection = ValueNotifier(-1);
final SpringModel animation = SpringModel();
double autoCompleteTop = 0;
double elevation = 200.0;
_AskImpl _ask;
final _askBinding = AskBarBinding();
final _suggestionProvider = SuggestionProviderProxy();
// Holds the suggestion results until query completes.
List<Suggestion> _suggestions = <Suggestion>[];
AskModel({this.startupContext}) {
_ask = _AskImpl(this);
StartupContext.fromStartupInfo()
.incoming
.connectToService(_suggestionProvider);
}
void focus(BuildContext context) =>
FocusScope.of(context).requestFocus(focusNode);
void unfocus() => focusNode.unfocus();
bool get isVisible => visibility.value;
ValueNotifier<ui.Image> imageFromSuggestion(Suggestion suggestion) {
final image = ValueNotifier<ui.Image>(null);
if (suggestion?.display?.icons?.first?.image?.vmo != null) {
final vmo = suggestion.display.icons.first.image.vmo;
_imageFromVmo(vmo).then((img) => image.value = img);
}
return image;
}
Future<ui.Image> _imageFromVmo(Vmo vmo) async {
final bytes = vmo.read(vmo.getSize().size).bytesAsUint8List();
final codec = await ui.instantiateImageCodec(bytes);
final frameInfo = await codec.getNextFrame();
final image = frameInfo.image;
codec.dispose();
return image;
}
// ignore: use_setters_to_change_properties
void load(double elevation) {
this.elevation = elevation;
}
void show() {
visibility.value = true;
_ask.fireVisible();
animation
..jump(0.8)
..target = 1.0;
controller.clear();
onQuery('');
}
void hide() {
_suggestions = <Suggestion>[];
suggestions.value = _suggestions;
selection.value = -1;
_ask.fireHidden();
visibility.value = false;
animation.jump(0.8);
controller.clear();
}
void onAsk(String query) {
// If there are no suggestions, do nothing.
if (suggestions.value.isEmpty) {
return;
}
// Use the top suggestion (highest confidence) if user did not select
// another suggestion from the list.
if (selection.value < 0) {
onSelect(suggestions.value.first);
} else {
onSelect(suggestions.value[selection.value]);
}
}
void onKey(RawKeyEvent event) {
RawKeyEventDataFuchsia data = event.data;
// We only process pure key down events: codePoint = 0 and modifiers = 0.
int newSelection = selection.value;
if (event is RawKeyDownEvent &&
suggestions.value.isNotEmpty &&
data.codePoint == 0 &&
data.modifiers == 0) {
switch (data.hidUsage) {
case _kEsc:
hide();
return;
case _kDownArrow:
newSelection++;
break;
case _kUpArrow:
newSelection--;
break;
case _kPageDown:
newSelection += 5;
break;
case _kPageUp:
newSelection -= 5;
break;
default:
return;
}
selection.value = newSelection.clamp(0, suggestions.value.length - 1);
controller
..text = suggestions.value[selection.value].display.details
..selection = TextSelection.fromPosition(TextPosition(
offset: controller.text.length,
));
}
}
void onQuery(String query) {
_suggestions = <Suggestion>[];
final queryListenerBinding = QueryListenerBinding();
_suggestionProvider.query(
queryListenerBinding.wrap(_QueryListenerImpl(this)),
UserInput(text: query),
_kMaxSuggestions,
);
}
void onSelect(Suggestion suggestion) {
_suggestionProvider.notifyInteraction(
suggestion.uuid,
Interaction(type: InteractionType.selected),
);
hide();
}
void advertise() {
startupContext.outgoing.addPublicService(
(InterfaceRequest<AskBar> request) => _askBinding.bind(_ask, request),
AskBar.$serviceName,
);
}
/// QueryListener.
void onQueryComplete() {
// Display suggestion list if Ask bar is still visible.
if (visibility.value) {
selection.value = -1;
suggestions.value = _suggestions;
}
}
// ignore: use_setters_to_change_properties
/// QueryListener.
void onQueryResults(List<Suggestion> suggestions) {
// Hold onto the suggestion in _suggestions until onQueryComplete is
// called.
_suggestions = suggestions;
}
}
class _AskImpl extends AskBar {
final AskModel askModel;
final _onHiddenStream = StreamController<void>();
final _onVisibleStream = StreamController<void>();
_AskImpl(this.askModel);
void close() {
_onHiddenStream.close();
_onVisibleStream.close();
}
void fireHidden() => _onHiddenStream.add(null);
void fireVisible() => _onVisibleStream.add(null);
@override
Future<void> show() async => askModel.show();
@override
Future<void> hide() async => askModel.hide();
@override
Future<void> load(double elevation) async => askModel.load(elevation);
@override
Stream<void> get onHidden => _onHiddenStream.stream;
@override
Stream<void> get onVisible => _onVisibleStream.stream;
}
class _QueryListenerImpl extends QueryListener {
final AskModel askModel;
_QueryListenerImpl(this.askModel);
@override
Future<void> onQueryComplete() async => askModel.onQueryComplete();
@override
Future<void> onQueryResults(List<Suggestion> suggestions) async =>
askModel.onQueryResults(suggestions);
}