From bb073e53b563ea33237d821fb83a290843bcec02 Mon Sep 17 00:00:00 2001 From: Daniel Mircea Date: Wed, 12 Jun 2024 20:40:44 +0300 Subject: [PATCH] files: Implement selections using shift key Previously only the `Control` allowed to add multiple entries to the current selection. This allows the use of `Shift` to select a range of entries. `Shift + Control` combinations are also supported now, similar to `nautilus`, `thunar`, etc. --- lib/backend/entity_selector.dart | 83 ++++++++++++++++++++++++++++++++ lib/backend/workspace.dart | 14 +++++- lib/widgets/grid.dart | 2 +- lib/widgets/table.dart | 2 +- lib/widgets/workspace.dart | 31 +++++------- pubspec.lock | 14 +++--- 6 files changed, 116 insertions(+), 30 deletions(-) create mode 100644 lib/backend/entity_selector.dart diff --git a/lib/backend/entity_selector.dart b/lib/backend/entity_selector.dart new file mode 100644 index 0000000..f6ec2c3 --- /dev/null +++ b/lib/backend/entity_selector.dart @@ -0,0 +1,83 @@ +import 'package:files/backend/entity_info.dart'; +import 'package:files/backend/workspace.dart'; +import 'package:flutter/services.dart'; + +enum EntitySelectionMode { + single, + multiple, + none, + mixed; + + factory EntitySelectionMode.fromKeys(Set keys) { + final hasShiftKey = keys.contains(LogicalKeyboardKey.shiftLeft) || + keys.contains(LogicalKeyboardKey.shiftRight); + final hasControlKey = keys.contains(LogicalKeyboardKey.controlLeft) || + keys.contains(LogicalKeyboardKey.controlRight); + + if (hasShiftKey && hasControlKey) { + return mixed; + } + + if (hasControlKey) { + return single; + } + + if (hasShiftKey) { + return multiple; + } + + return none; + } +} + +class EntitySelector { + final WorkspaceController _controller; + final EntityInfo _entity; + final EntitySelectionMode _mode; + + EntitySelector(this._controller, this._entity, this._mode); + + void perform() { + switch (_mode) { + case EntitySelectionMode.single: + _toggleEntitySelection(); + _controller.lastClickedEntityIndex = _getEntityIndex(); + break; + case EntitySelectionMode.multiple: + _controller.clearSelectedItems(); + _extendSelectionToEntity(); + break; + case EntitySelectionMode.mixed: + _extendSelectionToEntity(); + break; + default: + _controller.clearSelectedItems(); + _controller.addSelectedItem(_entity); + _controller.lastClickedEntityIndex = _getEntityIndex(); + } + } + + void _toggleEntitySelection() { + if (_controller.selectedItems.contains(_entity)) { + _controller.removeSelectedItem(_entity); + } else { + _controller.addSelectedItem(_entity); + } + } + + void _extendSelectionToEntity() { + final entityIndex = _getEntityIndex(); + final lastSelectedEntityIndex = _controller.lastClickedEntityIndex; + final entities = entityIndex < lastSelectedEntityIndex + ? _controller.currentInfo! + .sublist(entityIndex, lastSelectedEntityIndex + 1) + : _controller.currentInfo! + .sublist(lastSelectedEntityIndex, entityIndex + 1); + + _controller.addSelectedItems(entities); + } + + int _getEntityIndex() { + return _controller.currentInfo!.indexOf(_entity); + } +} diff --git a/lib/backend/workspace.dart b/lib/backend/workspace.dart index 5261f10..0f00ee6 100644 --- a/lib/backend/workspace.dart +++ b/lib/backend/workspace.dart @@ -34,6 +34,7 @@ class WorkspaceController with ChangeNotifier { SortType _sortType = SortType.name; WorkspaceView _view = WorkspaceView.table; // save on SharedPreferences? final List _selectedItems = []; + int lastClickedEntityIndex = 0; // States late TableViewState _tableState; @@ -130,6 +131,11 @@ class WorkspaceController with ChangeNotifier { notifyListeners(); } + void addSelectedItems(Iterable infos) { + _selectedItems.addAll(infos); + notifyListeners(); + } + void removeSelectedItem(EntityInfo info) { _selectedItems.remove(info); notifyListeners(); @@ -140,6 +146,12 @@ class WorkspaceController with ChangeNotifier { notifyListeners(); } + void resetSelection() { + lastClickedEntityIndex = 0; + _selectedItems.clear(); + notifyListeners(); + } + List get history => List.from(_history); int get historyOffset => _historyOffset; @@ -163,7 +175,7 @@ class WorkspaceController with ChangeNotifier { } clearCurrentInfo(); - clearSelectedItems(); + resetSelection(); await _directoryStream?.cancel(); await getInfoForDir(Directory(newDir)); _directoryStream = diff --git a/lib/widgets/grid.dart b/lib/widgets/grid.dart index 477e431..72a4770 100644 --- a/lib/widgets/grid.dart +++ b/lib/widgets/grid.dart @@ -48,7 +48,7 @@ class _FilesGridState extends State { final WorkspaceController controller = WorkspaceController.of(context); return GestureDetector( - onTap: controller.clearSelectedItems, + onTap: controller.resetSelection, child: Scrollbar( controller: scrollController, child: GridView.builder( diff --git a/lib/widgets/table.dart b/lib/widgets/table.dart index 41da4b9..eca6c20 100644 --- a/lib/widgets/table.dart +++ b/lib/widgets/table.dart @@ -56,7 +56,7 @@ class FilesTable extends StatelessWidget { return LayoutBuilder( builder: (context, constraints) { return GestureDetector( - onTap: () => controller.clearSelectedItems(), + onTap: () => controller.resetSelection(), child: DoubleScrollbars( horizontalController: horizontalController, verticalController: verticalController, diff --git a/lib/widgets/workspace.dart b/lib/widgets/workspace.dart index 0f07db1..74a0116 100644 --- a/lib/widgets/workspace.dart +++ b/lib/widgets/workspace.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:files/backend/entity_info.dart'; +import 'package:files/backend/entity_selector.dart'; import 'package:files/backend/fetch.dart'; import 'package:files/backend/path_parts.dart'; import 'package:files/backend/utils.dart'; @@ -135,25 +136,15 @@ class _FilesWorkspaceState extends State { } void _onEntityTap(EntityInfo entity) { - final bool selected = controller.selectedItems.contains(entity); - final Set keysPressed = - RawKeyboard.instance.keysPressed; - final bool multiSelect = keysPressed.contains( - LogicalKeyboardKey.controlLeft, - ) || - keysPressed.contains( - LogicalKeyboardKey.controlRight, - ); - - if (!multiSelect) controller.clearSelectedItems(); + final selectionMode = EntitySelectionMode.fromKeys( + HardwareKeyboard.instance.logicalKeysPressed, + ); - if (!selected && multiSelect) { - controller.addSelectedItem(entity); - } else if (selected && multiSelect) { - controller.removeSelectedItem(entity); - } else { - controller.addSelectedItem(entity); - } + EntitySelector( + controller, + entity, + selectionMode, + ).perform(); setState(() {}); } @@ -263,7 +254,7 @@ class _FilesWorkspaceState extends State { SizedBox( height: 32, child: Material( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( @@ -299,7 +290,7 @@ class _FilesWorkspaceState extends State { Text( "This Folder is Empty", style: TextStyle(fontSize: 17), - ) + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 1365a63..53b4179 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -133,10 +133,10 @@ packages: dependency: "direct main" description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" convert: dependency: transitive description: @@ -343,10 +343,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.8.0" material_design_icons_flutter: dependency: "direct main" description: @@ -359,10 +359,10 @@ packages: dependency: transitive description: name: meta - sha256: "12307e7f0605ce3da64cf0db90e5fcab0869f3ca03f76be6bb2991ce0a55e82b" + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.12.0" mime: dependency: "direct main" description: @@ -746,5 +746,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=3.0.0-134.0.dev <4.0.0" + dart: ">=3.3.0-0 <4.0.0" flutter: ">=3.3.0"