From 6432abd2a9f672daf3797efba7bf7fd7ea5724ed Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Thu, 5 Sep 2024 22:19:54 +0530 Subject: [PATCH] [client] Better mouse handling --- client/lib/app_shortcuts.dart | 1 + .../lib/ui/folder/folder_contents_view.dart | 86 +++++++++++-------- .../ui/folder/folder_selection_manager.dart | 32 +++---- .../lib/ui/folder/resource_details_row.dart | 16 ++-- 4 files changed, 73 insertions(+), 62 deletions(-) diff --git a/client/lib/app_shortcuts.dart b/client/lib/app_shortcuts.dart index 5e0b0171..64fb938a 100644 --- a/client/lib/app_shortcuts.dart +++ b/client/lib/app_shortcuts.dart @@ -5,6 +5,7 @@ enum SelectionMode { single, range, multi, + toggle, } class FocusUpIntent extends Intent { diff --git a/client/lib/ui/folder/folder_contents_view.dart b/client/lib/ui/folder/folder_contents_view.dart index 7b74fae4..688c4d05 100644 --- a/client/lib/ui/folder/folder_contents_view.dart +++ b/client/lib/ui/folder/folder_contents_view.dart @@ -24,7 +24,6 @@ class FolderContentsView extends StatefulWidget { class _FolderContentsViewState extends State { final Map nodes = {}; late List resources; - int focusIndex = -1; int selectionStartIndex = -1; @override @@ -55,33 +54,37 @@ class _FolderContentsViewState extends State { PreviousFocusIntent: CallbackAction(onInvoke: (i) => null), FocusUpIntent: CallbackAction(onInvoke: (i) { if (resources.isEmpty) return; + int focusIndex = context.read().focusIndex; final index = focusIndex == -1 ? resources.length : focusIndex > 0 ? min(focusIndex - 1, resources.length) : 0; - updateFocus(index, i.mode); + updateSelection(index, i.mode, true); return null; }), FocusDownIntent: CallbackAction(onInvoke: (i) { if (resources.isEmpty) return; + int focusIndex = context.read().focusIndex; final index = min(focusIndex + 1, resources.length - 1); - updateFocus(index, i.mode); + updateSelection(index, i.mode, true); return null; }), ToggleSelectionIntent: CallbackAction(onInvoke: (i) { - toggleSelection(); + final focusIndex = context.read().focusIndex; + updateSelection(focusIndex, SelectionMode.toggle, true); return null; }), SelectAllIntent: CallbackAction(onInvoke: (i) { - context.read().setSelected(Set.of(resources.map((r) => r.id))); + context.read().update(selected: Set.of(resources.map((r) => r.id)), showFocus: true); return null; }), DismissIntent: CallbackAction(onInvoke: (i) { + int focusIndex = context.read().focusIndex; if (focusIndex >= 0) { - context.read().setSelected({resources[min(focusIndex, resources.length)].id}); + updateSelection(focusIndex, SelectionMode.single, true); } else { - context.read().setSelected(const {}); + context.read().update(selected: const {}, showFocus: true); } return null; }), @@ -90,6 +93,7 @@ class _FolderContentsViewState extends State { return null; }), ActivateIntent: CallbackAction(onInvoke: (i) { + int focusIndex = context.read().focusIndex; open(resources[focusIndex]); return null; }), @@ -100,55 +104,65 @@ class _FolderContentsViewState extends State { }, child: Focus( autofocus: true, + descendantsAreFocusable: false, child: ListView.builder( itemCount: resources.length, itemBuilder: (context, index) { final r = resources[index]; - return Focus( - focusNode: getFocusNode(r), - child: ResourceDetailsRow( - r: r, - onTap: () { - final mode = HardwareKeyboard.instance.isControlPressed - ? SelectionMode.multi - : HardwareKeyboard.instance.isShiftPressed - ? SelectionMode.range - : SelectionMode.single; - updateFocus(index, mode); - if (mode == SelectionMode.multi) { - toggleSelection(); - } - }, - onDoubleTap: () => open(r)), - ); + bool deferHandling = false; + return ResourceDetailsRow( + r: r, + onTapDown: (details) { + final mode = HardwareKeyboard.instance.isControlPressed + ? SelectionMode.toggle + : HardwareKeyboard.instance.isShiftPressed + ? SelectionMode.range + : SelectionMode.single; + if (context.read().isSelected(r.id) && mode == SelectionMode.single) { + deferHandling = true; + } else { + deferHandling = false; + updateSelection(index, mode, false); + } + }, + onTap: () { + if (deferHandling) { + updateSelection(index, SelectionMode.single, false); + } + }, + onDoubleTap: () => open(r)); }, ), ), ); } - void updateFocus(int index, SelectionMode mode) { + void updateSelection(int index, SelectionMode mode, bool highlight) { + Set? selected; switch (mode) { case SelectionMode.range: if (selectionStartIndex == -1) { selectionStartIndex = index; } - final selected = Set.of(resources.getRange(min(selectionStartIndex, index), max(selectionStartIndex, index) + 1).map((r) => r.id)); - context.read().setSelected(selected); + selected = Set.of(resources.getRange(min(selectionStartIndex, index), max(selectionStartIndex, index) + 1).map((r) => r.id)); break; case SelectionMode.single: - context.read().setSelected({resources[index].id}); + selected = {resources[index].id}; + selectionStartIndex = index; + break; + case SelectionMode.toggle: + selected = Set.of(context.read().selected); + if (!selected.add(resources[index].id)) { + selected.remove(resources[index].id); + } selectionStartIndex = index; break; case SelectionMode.multi: break; } - getFocusNode(resources[index]).requestFocus(); - focusIndex = index; - } - - void toggleSelection() { - selectionStartIndex = context.read().toggleSelected(resources[focusIndex].id) ? focusIndex : -1; + context + .read() + .update(selected: selected, focusId: index < resources.length ? resources[index].id : null, focusIndex: index, showFocus: highlight); } void open(Resource r) { @@ -169,8 +183,4 @@ class _FolderContentsViewState extends State { account.addAction(ResourceDeleteAction(r: r)); } } - - FocusNode getFocusNode(Resource r) { - return nodes.putIfAbsent(r.id, () => FocusNode(descendantsAreTraversable: false)); - } } diff --git a/client/lib/ui/folder/folder_selection_manager.dart b/client/lib/ui/folder/folder_selection_manager.dart index 06b69626..57020fad 100644 --- a/client/lib/ui/folder/folder_selection_manager.dart +++ b/client/lib/ui/folder/folder_selection_manager.dart @@ -2,8 +2,11 @@ import 'package:state_notifier/state_notifier.dart'; class FolderSelectionState { final Set selected; + final String focusId; + final int focusIndex; + final bool showFocus; - FolderSelectionState({required this.selected}); + FolderSelectionState({required this.selected, required this.focusId, required this.focusIndex, required this.showFocus}); bool isSelected(String id) { return selected.contains(id); @@ -11,20 +14,19 @@ class FolderSelectionState { } class FolderSelectionManager extends StateNotifier { - FolderSelectionManager() : super(FolderSelectionState(selected: {})); + FolderSelectionManager() : super(FolderSelectionState(selected: {}, focusId: "", focusIndex: -1, showFocus: false)); - void setSelected(Set ids) { - state = FolderSelectionState(selected: ids); - } - - bool toggleSelected(String id) { - final selected = Set.of(state.selected); - bool added = true; - if (!selected.add(id)) { - selected.remove(id); - added = false; - } - state = FolderSelectionState(selected: selected); - return added; + void update({ + Set? selected, + String? focusId, + int? focusIndex, + bool? showFocus, + }) { + state = FolderSelectionState( + selected: selected ?? state.selected, + focusId: focusId ?? state.focusId, + focusIndex: focusIndex ?? state.focusIndex, + showFocus: showFocus ?? state.showFocus, + ); } } diff --git a/client/lib/ui/folder/resource_details_row.dart b/client/lib/ui/folder/resource_details_row.dart index 5b1e2cb0..bd04917d 100644 --- a/client/lib/ui/folder/resource_details_row.dart +++ b/client/lib/ui/folder/resource_details_row.dart @@ -6,11 +6,12 @@ import 'package:provider/provider.dart'; class ResourceDetailsRow extends StatelessWidget { final Resource r; + final Function(TapDownDetails)? onTapDown; final Function()? onTap; final Function()? onDoubleTap; final Function()? onSecondaryTap; - ResourceDetailsRow({required this.r, this.onTap, this.onDoubleTap, this.onSecondaryTap}) : super(key: ValueKey(r.id)); + ResourceDetailsRow({required this.r, this.onTapDown, this.onTap, this.onDoubleTap, this.onSecondaryTap}) : super(key: ValueKey(r.id)); @override Widget build(BuildContext context) { @@ -29,7 +30,7 @@ class ResourceDetailsRow extends StatelessWidget { } Widget buildRow(BuildContext context) { - final focussed = Focus.isAt(context); + final focussed = context.select((state) => state.focusId == r.id && state.showFocus); final selected = context.select((state) => state.isSelected(r.id)); return Draggable( @@ -51,14 +52,11 @@ class ResourceDetailsRow extends StatelessWidget { ), ); }), - childWhenDragging: Builder(builder: (context) { - return Opacity(opacity: 0.5, child: buildListTile(context, focussed, selected)); - }), child: GestureDetector( - onTapDown: selected ? null : (details) => onTap?.call(), - onTap: selected ? () => onTap?.call() : null, - onDoubleTap: () => onDoubleTap?.call(), - onSecondaryTap: () => onSecondaryTap?.call(), + onTapDown: onTapDown, + onTap: onTap, + onDoubleTap: onDoubleTap, + onSecondaryTap: onSecondaryTap, child: buildListTile(context, focussed, selected), ), );