[client] Better mouse handling

This commit is contained in:
Abhishek Shroff
2024-09-05 22:19:54 +05:30
parent 30a1d3739a
commit 6432abd2a9
4 changed files with 73 additions and 62 deletions

View File

@@ -5,6 +5,7 @@ enum SelectionMode {
single,
range,
multi,
toggle,
}
class FocusUpIntent extends Intent {

View File

@@ -24,7 +24,6 @@ class FolderContentsView extends StatefulWidget {
class _FolderContentsViewState extends State<FolderContentsView> {
final Map<String, FocusNode> nodes = {};
late List<Resource> resources;
int focusIndex = -1;
int selectionStartIndex = -1;
@override
@@ -55,33 +54,37 @@ class _FolderContentsViewState extends State<FolderContentsView> {
PreviousFocusIntent: CallbackAction<PreviousFocusIntent>(onInvoke: (i) => null),
FocusUpIntent: CallbackAction<FocusUpIntent>(onInvoke: (i) {
if (resources.isEmpty) return;
int focusIndex = context.read<FolderSelectionState>().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<FocusDownIntent>(onInvoke: (i) {
if (resources.isEmpty) return;
int focusIndex = context.read<FolderSelectionState>().focusIndex;
final index = min(focusIndex + 1, resources.length - 1);
updateFocus(index, i.mode);
updateSelection(index, i.mode, true);
return null;
}),
ToggleSelectionIntent: CallbackAction<ToggleSelectionIntent>(onInvoke: (i) {
toggleSelection();
final focusIndex = context.read<FolderSelectionState>().focusIndex;
updateSelection(focusIndex, SelectionMode.toggle, true);
return null;
}),
SelectAllIntent: CallbackAction<SelectAllIntent>(onInvoke: (i) {
context.read<FolderSelectionManager>().setSelected(Set.of(resources.map((r) => r.id)));
context.read<FolderSelectionManager>().update(selected: Set.of(resources.map((r) => r.id)), showFocus: true);
return null;
}),
DismissIntent: CallbackAction<DismissIntent>(onInvoke: (i) {
int focusIndex = context.read<FolderSelectionState>().focusIndex;
if (focusIndex >= 0) {
context.read<FolderSelectionManager>().setSelected({resources[min(focusIndex, resources.length)].id});
updateSelection(focusIndex, SelectionMode.single, true);
} else {
context.read<FolderSelectionManager>().setSelected(const {});
context.read<FolderSelectionManager>().update(selected: const {}, showFocus: true);
}
return null;
}),
@@ -90,6 +93,7 @@ class _FolderContentsViewState extends State<FolderContentsView> {
return null;
}),
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: (i) {
int focusIndex = context.read<FolderSelectionState>().focusIndex;
open(resources[focusIndex]);
return null;
}),
@@ -100,55 +104,65 @@ class _FolderContentsViewState extends State<FolderContentsView> {
},
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<FolderSelectionState>().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<String>? 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<FolderSelectionManager>().setSelected(selected);
selected = Set.of(resources.getRange(min(selectionStartIndex, index), max(selectionStartIndex, index) + 1).map((r) => r.id));
break;
case SelectionMode.single:
context.read<FolderSelectionManager>().setSelected({resources[index].id});
selected = {resources[index].id};
selectionStartIndex = index;
break;
case SelectionMode.toggle:
selected = Set.of(context.read<FolderSelectionState>().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<FolderSelectionManager>().toggleSelected(resources[focusIndex].id) ? focusIndex : -1;
context
.read<FolderSelectionManager>()
.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<FolderContentsView> {
account.addAction(ResourceDeleteAction(r: r));
}
}
FocusNode getFocusNode(Resource r) {
return nodes.putIfAbsent(r.id, () => FocusNode(descendantsAreTraversable: false));
}
}

View File

@@ -2,8 +2,11 @@ import 'package:state_notifier/state_notifier.dart';
class FolderSelectionState {
final Set<String> 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<FolderSelectionState> {
FolderSelectionManager() : super(FolderSelectionState(selected: {}));
FolderSelectionManager() : super(FolderSelectionState(selected: {}, focusId: "", focusIndex: -1, showFocus: false));
void setSelected(Set<String> 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<String>? 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,
);
}
}

View File

@@ -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<FolderSelectionState, bool>((state) => state.focusId == r.id && state.showFocus);
final selected = context.select<FolderSelectionState, bool>((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),
),
);