feat: implement keyboard shortcuts

Added shortcuts for the following actions:

- search chats
- start chat
- chat details
- show widgets
- cycle accounts
- switch to account $i
- toggle emoji picker
- send file

Related: #849

Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
This commit is contained in:
TheOneWithTheBraid 2022-03-01 20:14:49 +01:00
commit 824fcfc27c
7 changed files with 400 additions and 180 deletions

View file

@ -2,9 +2,11 @@ import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:animations/animations.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
import 'package:matrix/matrix.dart';
import 'package:vrouter/vrouter.dart';
@ -43,16 +45,16 @@ class ChatListView extends StatelessWidget {
? null
: Theme.of(context).colorScheme.primary,
),
leading: selectMode == SelectMode.normal
? Matrix.of(context).isMultiAccount
? ClientChooserButton(controller)
: null
: IconButton(
tooltip: L10n.of(context)!.cancel,
icon: const Icon(Icons.close_outlined),
onPressed: controller.cancelAction,
color: Theme.of(context).colorScheme.primary,
),
leading: Matrix.of(context).isMultiAccount
? ClientChooserButton(controller)
: selectMode == SelectMode.normal
? null
: IconButton(
tooltip: L10n.of(context)!.cancel,
icon: const Icon(Icons.close_outlined),
onPressed: controller.cancelAction,
color: Theme.of(context).colorScheme.primary,
),
centerTitle: false,
actions: selectMode == SelectMode.share
? null
@ -93,11 +95,20 @@ class ChatListView extends StatelessWidget {
),
]
: [
IconButton(
icon: const Icon(Icons.search_outlined),
tooltip: L10n.of(context)!.search,
onPressed: () =>
KeyBoardShortcuts(
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.keyF
},
onKeysPressed: () =>
VRouter.of(context).to('/search'),
helpLabel: L10n.of(context)!.search,
child: IconButton(
icon: const Icon(Icons.search_outlined),
tooltip: L10n.of(context)!.search,
onPressed: () =>
VRouter.of(context).to('/search'),
),
),
if (selectMode == SelectMode.normal)
IconButton(
@ -213,12 +224,21 @@ class ChatListView extends StatelessWidget {
Expanded(child: _ChatListViewBody(controller)),
]),
floatingActionButton: selectMode == SelectMode.normal
? FloatingActionButton.extended(
isExtended: controller.scrolledToTop,
onPressed: () =>
? KeyBoardShortcuts(
child: FloatingActionButton.extended(
isExtended: controller.scrolledToTop,
onPressed: () =>
VRouter.of(context).to('/newprivatechat'),
icon: const Icon(CupertinoIcons.chat_bubble),
label: Text(L10n.of(context)!.newChat),
),
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.keyN
},
onKeysPressed: () =>
VRouter.of(context).to('/newprivatechat'),
icon: const Icon(CupertinoIcons.chat_bubble),
label: Text(L10n.of(context)!.newChat),
helpLabel: L10n.of(context)!.newChat,
)
: null,
bottomNavigationBar: Column(

View file

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/widgets/avatar.dart';
@ -8,6 +11,7 @@ import 'chat_list.dart';
class ClientChooserButton extends StatelessWidget {
final ChatListController controller;
const ClientChooserButton(this.controller, {Key? key}) : super(key: key);
List<PopupMenuEntry<Object>> _bundleMenuItems(BuildContext context) {
@ -81,26 +85,137 @@ class ClientChooserButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final matrix = Matrix.of(context);
int clientCount = 0;
matrix.accountBundles.forEach((key, value) => clientCount += value.length);
return Center(
child: FutureBuilder<Profile>(
future: matrix.client.ownProfile,
builder: (context, snapshot) => PopupMenuButton<Object>(
child: Avatar(
mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ?? matrix.client.userID!.localpart,
size: 28,
fontSize: 12,
),
onSelected: (Object object) {
if (object is Client) {
controller.setActiveClient(object);
} else if (object is String) {
controller.setActiveBundle(object);
}
},
itemBuilder: _bundleMenuItems,
builder: (context, snapshot) => Stack(
alignment: Alignment.center,
children: [
...List.generate(
clientCount,
(index) => KeyBoardShortcuts(
child: Container(),
keysToPress: _buildKeyboardShortcut(index + 1),
helpLabel: L10n.of(context)!.switchToAccount(index + 1),
onKeysPressed: () => _handleKeyboardShortcut(matrix, index),
),
),
KeyBoardShortcuts(
child: Container(),
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.tab
},
helpLabel: L10n.of(context)!.nextAccount,
onKeysPressed: () => _nextAccount(matrix),
),
KeyBoardShortcuts(
child: Container(),
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.tab
},
helpLabel: L10n.of(context)!.previousAccount,
onKeysPressed: () => _previousAccount(matrix),
),
PopupMenuButton<Object>(
child: Avatar(
mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ??
matrix.client.userID!.localpart,
size: 28,
fontSize: 12,
),
onSelected: _clientSelected,
itemBuilder: _bundleMenuItems,
),
],
),
),
);
}
Set<LogicalKeyboardKey>? _buildKeyboardShortcut(int index) {
if (index > 0 && index < 10) {
return {
LogicalKeyboardKey.altLeft,
LogicalKeyboardKey(0x00000000030 + index)
};
} else {
return null;
}
}
void _clientSelected(Object object) {
if (object is Client) {
controller.setActiveClient(object);
} else if (object is String) {
controller.setActiveBundle(object);
}
}
void _handleKeyboardShortcut(MatrixState matrix, int index) {
final bundles = matrix.accountBundles.keys.toList()
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId
? 0
: a.isValidMatrixId && !b.isValidMatrixId
? -1
: 1);
// beginning from end if negative
if (index < 0) {
int clientCount = 0;
matrix.accountBundles
.forEach((key, value) => clientCount += value.length);
_handleKeyboardShortcut(matrix, clientCount);
}
for (final bundleName in bundles) {
final bundle = matrix.accountBundles[bundleName];
if (bundle != null) {
if (index < bundle.length) {
return _clientSelected(bundle[index]!);
} else {
index -= bundle.length;
}
}
}
// if index too high, restarting from 0
_handleKeyboardShortcut(matrix, 0);
}
int? _shortcutIndexOfClient(MatrixState matrix, Client client) {
int index = 0;
final bundles = matrix.accountBundles.keys.toList()
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId
? 0
: a.isValidMatrixId && !b.isValidMatrixId
? -1
: 1);
for (final bundleName in bundles) {
final bundle = matrix.accountBundles[bundleName];
if (bundle == null) return null;
if (bundle.contains(client)) {
return index + bundle.indexOf(client);
} else {
index += bundle.length;
}
}
return null;
}
void _nextAccount(MatrixState matrix) {
final client = matrix.client;
final lastIndex = _shortcutIndexOfClient(matrix, client);
_handleKeyboardShortcut(matrix, lastIndex! + 1);
}
void _previousAccount(MatrixState matrix) {
final client = matrix.client;
final lastIndex = _shortcutIndexOfClient(matrix, client);
_handleKeyboardShortcut(matrix, lastIndex! - 1);
}
}