blob: 02db1c9a797d6e1aaacffae2a42f4a13ca374096 [file] [log] [blame]
// Copyright 2020 The Chromium 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 'package:flutter/material.dart';
import 'package:vm_service/vm_service.dart';
import '../common_widgets.dart';
import '../config_specific/host_platform/host_platform.dart';
import '../theme.dart';
import '../tree.dart';
import '../utils.dart';
import 'debugger_controller.dart';
import 'debugger_model.dart';
import 'debugger_screen.dart';
const containerIcon = Icons.folder;
const libraryIcon = Icons.insert_chart;
const listItemHeight = 40.0;
/// Picker that takes a list of scripts and allows filtering and selection of
/// items.
class ScriptPicker extends StatefulWidget {
const ScriptPicker({
Key key,
@required this.controller,
@required this.scripts,
@required this.onSelected,
this.libraryFilterFocusNode,
}) : super(key: key);
final DebuggerController controller;
final List<ScriptRef> scripts;
final void Function(ScriptLocation scriptRef) onSelected;
final FocusNode libraryFilterFocusNode;
@override
ScriptPickerState createState() => ScriptPickerState();
}
class ScriptPickerState extends State<ScriptPicker> {
// TODO(devoncarew): How to retain the filter text state?
final _filterController = TextEditingController();
final _maxAutoExpandChildCount = 3;
List<ObjRef> _items = [];
List<ObjRef> _filteredItems = [];
List<FileNode> _rootScriptNodes;
@override
void initState() {
super.initState();
_updateFiltered();
}
@override
void didUpdateWidget(ScriptPicker oldWidget) {
super.didUpdateWidget(oldWidget);
updateFilter();
}
void updateFilter() {
setState(_updateFiltered);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isMacOS = HostPlatform.instance.isMacOS;
// Re-calculate the tree of scripts if necessary.
_rootScriptNodes ??= FileNode.createRootsFrom(_filteredItems);
return OutlineDecoration(
child: Column(
children: [
AreaPaneHeader(
title: const Text('Libraries'),
needsTopBorder: false,
actions: [
CountBadge(
filteredItems: _filteredItems,
items: _items,
),
],
),
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: theme.focusColor),
),
),
child: Padding(
padding: const EdgeInsets.all(denseSpacing),
child: SizedBox(
height: defaultTextFieldHeight,
child: TextField(
decoration: InputDecoration(
labelText:
'Filter (${focusLibraryFilterKeySet.describeKeys(isMacOS: isMacOS)})',
border: const OutlineInputBorder(),
),
controller: _filterController,
onChanged: (value) => updateFilter(),
style: Theme.of(context).textTheme.bodyText2,
focusNode: widget.libraryFilterFocusNode,
),
),
),
),
if (_isLoading) const CenteredCircularProgressIndicator(),
if (!_isLoading)
Expanded(
child: TreeView<FileNode>(
onTraverse: (node) {
// Auto expand children when there are minimal search results.
if (_filterController.text.isNotEmpty &&
node.children.length <= _maxAutoExpandChildCount) {
node.expand();
}
},
itemExtent: listItemHeight,
dataRoots: _rootScriptNodes,
dataDisplayProvider: (item, onTap) =>
_displayProvider(context, item, onTap),
),
),
],
),
);
}
Widget _displayProvider(
BuildContext context,
FileNode node,
VoidCallback onTap,
) {
return Tooltip(
waitDuration: tooltipWait,
preferBelow: false,
message: node.name,
child: Material(
child: InkWell(
onTap: () {
if (node.hasScript) {
_handleSelected(node.scriptRef);
}
onTap();
},
child: Row(
children: [
Icon(
node.hasScript ? libraryIcon : containerIcon,
size: defaultIconSize,
),
const SizedBox(width: densePadding),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!node.hasScript)
Text(
node.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
else ...[
Text(
node.fileName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
node.scriptRef.uri,
style: Theme.of(context).subtleTextStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
]
],
),
),
],
),
),
),
);
}
bool get _isLoading => widget.scripts.isEmpty;
void _updateFiltered() {
final filterText = _filterController.text.trim().toLowerCase();
_items = widget.scripts;
_filteredItems = widget.scripts
.where((ref) => ref.uri.caseInsensitiveFuzzyMatch(filterText))
.toList();
// Remove the cached value here; it'll be re-computed the next time we need
// it.
_rootScriptNodes = null;
}
void _handleSelected(ObjRef ref) async {
if (ref is ScriptRef) {
widget.onSelected(ScriptLocation(ref));
} else if (ref is ClassRef) {
final obj = await widget.controller.getObject(ref);
final location = (obj as Class).location;
final script = await widget.controller.getScript(location.script);
final pos =
widget.controller.calculatePosition(script, location.tokenPos);
widget.onSelected(ScriptLocation(script, location: pos));
} else {
assert(false, 'unexpected object reference: ${ref.type}');
}
}
}
class CountBadge extends StatelessWidget {
const CountBadge({
@required this.filteredItems,
@required this.items,
});
final List<ObjRef> filteredItems;
final List<ObjRef> items;
@override
Widget build(BuildContext context) {
if (filteredItems.length == items.length) {
return Badge('${nf.format(items.length)}');
} else {
return Badge('${nf.format(filteredItems.length)} of '
'${nf.format(items.length)}');
}
}
}