Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 38 additions & 9 deletions lib/features/command_palette/command_palette_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class _CommandPaletteDialogState extends State<CommandPaletteDialog> {
final _focusNode = FocusNode();
final _scroll = material.ScrollController();
var _selected = 0;
List<QueryaCommand> _cachedHits = const [];
String _cachedQuery = '';

/// Set by keyboard navigation: the list moves under a still pointer, and the
/// hover that causes must not take the selection back. Cleared as soon as
Expand All @@ -51,20 +53,32 @@ class _CommandPaletteDialogState extends State<CommandPaletteDialog> {
@override
void initState() {
super.initState();
_controller.addListener(() {
setState(() => _selected = 0);
if (_scroll.hasClients) _scroll.jumpTo(0);
});
_updateHits();
_controller.addListener(_onQueryChanged);
QueryaCommandRegistry.instance.addListener(_onRegistryChanged);
}

@override
void dispose() {
QueryaCommandRegistry.instance.removeListener(_onRegistryChanged);
_controller.removeListener(_onQueryChanged);
_scroll.dispose();
_controller.dispose();
_focusNode.dispose();
super.dispose();
}

void _onQueryChanged() {
_updateHits();
setState(() => _selected = 0);
if (_scroll.hasClients) _scroll.jumpTo(0);
}

void _onRegistryChanged() {
if (!mounted) return;
setState(_updateHits);
}

bool get _objectPrefix {
final t = _controller.text;
return t.startsWith('#') || t.startsWith('@');
Expand All @@ -73,10 +87,23 @@ class _CommandPaletteDialogState extends State<CommandPaletteDialog> {
String get _objectQuery =>
_objectPrefix ? _controller.text.substring(1) : '';

List<QueryaCommand> get _hits {
if (_objectPrefix) return const [];
void _updateHits() {
if (_objectPrefix) {
_cachedHits = const [];
_cachedQuery = _controller.text;
return;
}
final q = _controller.text;
final ctx = widget.hostContext.mounted ? widget.hostContext : context;
return QueryaCommandRegistry.instance.search(_controller.text, context: ctx);
_cachedHits = QueryaCommandRegistry.instance.search(q, context: ctx);
_cachedQuery = q;
}

List<QueryaCommand> get _hits {
if (_cachedQuery != _controller.text) {
_updateHits();
}
return _cachedHits;
}

void _openQuickSwitcher() {
Expand Down Expand Up @@ -201,9 +228,11 @@ class _CommandPaletteDialogState extends State<CommandPaletteDialog> {
child: material.InkWell(
key: ValueKey(command.id),
onTap: () => _run(command),
onHover: (_) {
onHover: (hovered) {
if (_ignoreHover) return;
setState(() => _selected = index);
if (hovered && _selected != index) {
setState(() => _selected = index);
}
},
borderRadius: BorderRadius.circular(6),
child: DecoratedBox(
Expand Down
6 changes: 4 additions & 2 deletions lib/features/command_palette/quick_switcher_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,11 @@ class _QuickSwitcherDialogState extends State<QuickSwitcherDialog> {
child: material.InkWell(
key: ValueKey(object.id),
onTap: () => _open(object),
onHover: (_) {
onHover: (hovered) {
if (_ignoreHover) return;
setState(() => _selected = index);
if (hovered && _selected != index) {
setState(() => _selected = index);
}
},
borderRadius: BorderRadius.circular(6),
child: DecoratedBox(
Expand Down
44 changes: 44 additions & 0 deletions test/features/command_palette/command_palette_dialog_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/actions/querya_command.dart';
Expand Down Expand Up @@ -53,6 +54,49 @@ void main() {
expect(find.byKey(const ValueKey('querya.connection.new')), findsNothing);
});

testWidgets('hovering over an item updates selection without redundant search', (tester) async {
registry.register(
QueryaCommand(
id: 'test.cmd.1',
title: 'Command 1',
category: 'Test',
execute: (_) {},
),
);
registry.register(
QueryaCommand(
id: 'test.cmd.2',
title: 'Command 2',
category: 'Test',
execute: (_) {},
),
);
await pumpPalette(tester);

final firstItem = find.byKey(const ValueKey('test.cmd.1'));
final secondItem = find.byKey(const ValueKey('test.cmd.2'));

expect(firstItem, findsOneWidget);
expect(secondItem, findsOneWidget);

final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: tester.getCenter(firstItem));
await tester.pump();

// Hover over second item
await gesture.moveTo(tester.getCenter(secondItem));
await tester.pump();

final inkWellFinder = find.byKey(const ValueKey('test.cmd.2'));
final decoratedBox = tester.widget<DecoratedBox>(
find.descendant(of: inkWellFinder, matching: find.byType(DecoratedBox)),
);
final boxDecoration = decoratedBox.decoration as BoxDecoration;
expect(boxDecoration.color, isNot(equals(Colors.transparent)));

await gesture.removePointer();
});

testWidgets('Escape closes the palette', (tester) async {
registry.ensureCoreDefaults();
await pumpPalette(tester);
Expand Down
Loading