Skip to content

Commit

Permalink
Tab left+right navigation shortcuts (#70)
Browse files Browse the repository at this point in the history
Adds shortcuts for Ctrl + Tab and Ctrl + Shift + Tab to navigate tabs
left and right
  • Loading branch information
Gold872 authored Jul 19, 2024
1 parent 153046f commit c36cb6d
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 0 deletions.
48 changes: 48 additions & 0 deletions lib/pages/dashboard_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,30 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
},
);
}
// Move to next tab (Ctrl + Tab)
hotKeyManager.register(
HotKey(
LogicalKeyboardKey.tab,
modifiers: [KeyModifier.control],
),
callback: () {
if (ModalRoute.of(context)?.isCurrent ?? false) {
_moveToNextTab();
}
},
);
// Move to prevoius tab (Ctrl + Shift + Tab)
hotKeyManager.register(
HotKey(
LogicalKeyboardKey.tab,
modifiers: [KeyModifier.control, KeyModifier.shift],
),
callback: () {
if (ModalRoute.of(context)?.isCurrent ?? false) {
_moveToPreviousTab();
}
},
);
// Move Tab Left (Ctrl + <-)
hotKeyManager.register(
HotKey(
Expand Down Expand Up @@ -1287,6 +1311,30 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
});
}

void _moveToNextTab() {
int moveIndex = _currentTabIndex + 1;

if (moveIndex >= _tabData.length) {
moveIndex = 0;
}

setState(() {
_currentTabIndex = moveIndex;
});
}

void _moveToPreviousTab() {
int moveIndex = _currentTabIndex - 1;

if (moveIndex < 0) {
moveIndex = _tabData.length - 1;
}

setState(() {
_currentTabIndex = moveIndex;
});
}

@override
Widget build(BuildContext context) {
TextStyle? menuTextStyle = Theme.of(context).textTheme.bodySmall;
Expand Down
8 changes: 8 additions & 0 deletions lib/services/hotkey_manager.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'package:collection/collection.dart';
Expand Down Expand Up @@ -45,6 +46,13 @@ class HotKeyManager {
_initialized = true;
}

@visibleForTesting
void tearDown() {
_initialized = false;
_hotKeyList.clear();
_callbackMap.clear();
}

int _getNumberModifiersPressed() {
int count = 0;

Expand Down
258 changes: 258 additions & 0 deletions test/pages/dashboard_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:io';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'package:elegant_notification/elegant_notification.dart';
import 'package:flutter_test/flutter_test.dart';
Expand All @@ -12,6 +13,7 @@ import 'package:titlebar_buttons/titlebar_buttons.dart';

import 'package:elastic_dashboard/pages/dashboard_page.dart';
import 'package:elastic_dashboard/services/field_images.dart';
import 'package:elastic_dashboard/services/hotkey_manager.dart';
import 'package:elastic_dashboard/services/nt4_client.dart';
import 'package:elastic_dashboard/services/settings.dart';
import 'package:elastic_dashboard/widgets/custom_appbar.dart';
Expand Down Expand Up @@ -53,6 +55,10 @@ void main() {
preferences = await SharedPreferences.getInstance();
});

tearDown(() {
hotKeyManager.tearDown();
});

testWidgets('Dashboard page loading offline', (widgetTester) async {
FlutterError.onError = ignoreOverflowErrors;

Expand Down Expand Up @@ -133,6 +139,30 @@ void main() {
expect(jsonString, preferences.getString(PrefKeys.layout));
});

testWidgets('Save layout (shortcut)', (widgetTester) async {
FlutterError.onError = ignoreOverflowErrors;

await widgetTester.pumpWidget(
MaterialApp(
home: DashboardPage(
ntConnection: createMockOfflineNT4(),
preferences: preferences,
version: '0.0.0.0',
),
),
);

await widgetTester.pumpAndSettle();

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.control);

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.keyS);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.keyS);
await widgetTester.pumpAndSettle();

expect(jsonString, preferences.getString(PrefKeys.layout));
});

testWidgets('Add widget dialog (widgets)', (widgetTester) async {
FlutterError.onError = ignoreOverflowErrors;
createMockOnlineNT4();
Expand Down Expand Up @@ -567,6 +597,32 @@ void main() {
expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(3));
});

testWidgets('Creating new tab (shortcut)', (widgetTester) async {
FlutterError.onError = ignoreOverflowErrors;

await widgetTester.pumpWidget(
MaterialApp(
home: DashboardPage(
ntConnection: createMockOfflineNT4(),
preferences: preferences,
version: '0.0.0.0',
),
),
);

await widgetTester.pumpAndSettle();

expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2));

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.control);
await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.keyT);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.keyT);

await widgetTester.pumpAndSettle();

expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(3));
});

testWidgets('Closing tab', (widgetTester) async {
FlutterError.onError = ignoreOverflowErrors;

Expand Down Expand Up @@ -609,6 +665,42 @@ void main() {
expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(1));
});

testWidgets('Closing tab (shortcut)', (widgetTester) async {
FlutterError.onError = ignoreOverflowErrors;

await widgetTester.pumpWidget(
MaterialApp(
home: DashboardPage(
ntConnection: createMockOfflineNT4(),
preferences: preferences,
version: '0.0.0.0',
),
),
);

await widgetTester.pumpAndSettle();

expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2));

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.control);
await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.keyW);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.keyW);

await widgetTester.pumpAndSettle();

expect(find.text('Confirm Tab Close', skipOffstage: false), findsOneWidget);

final confirmButton =
find.widgetWithText(TextButton, 'OK', skipOffstage: false);

expect(confirmButton, findsOneWidget);

await widgetTester.tap(confirmButton);
await widgetTester.pumpAndSettle();

expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(1));
});

testWidgets('Reordering tabs', (widgetTester) async {
FlutterError.onError = ignoreOverflowErrors;

Expand Down Expand Up @@ -667,6 +759,172 @@ void main() {
expect(editableTabBarWidget().currentIndex, 0);
});

testWidgets('Reordering tabs (shortcut)', (widgetTester) async {
FlutterError.onError = ignoreOverflowErrors;

await widgetTester.pumpWidget(
MaterialApp(
home: DashboardPage(
ntConnection: createMockOfflineNT4(),
preferences: preferences,
version: '0.0.0.0',
),
),
);

await widgetTester.pumpAndSettle();

expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2));

final editableTabBar = find.byType(EditableTabBar);

expect(editableTabBar, findsOneWidget);

editableTabBarWidget() =>
(editableTabBar.evaluate().first.widget as EditableTabBar);

expect(editableTabBarWidget().currentIndex, 0);

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.control);

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.arrowLeft);
await widgetTester.pumpAndSettle();

expect(editableTabBarWidget().currentIndex, 0,
reason: 'Tab index should not change since index is 0');

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.arrowRight);
await widgetTester.pumpAndSettle();

expect(editableTabBarWidget().currentIndex, 1);

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.arrowRight);
await widgetTester.pumpAndSettle();

expect(editableTabBarWidget().currentIndex, 1,
reason:
'Tab index should not change since index is equal to number of tabs');

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.arrowLeft);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.arrowLeft);
await widgetTester.pumpAndSettle();

expect(editableTabBarWidget().currentIndex, 0);
});

testWidgets('Navigate tabs left right (shortcut)', (widgetTester) async {
FlutterError.onError = ignoreOverflowErrors;

await widgetTester.pumpWidget(
MaterialApp(
home: DashboardPage(
ntConnection: createMockOfflineNT4(),
preferences: preferences,
version: '0.0.0.0',
),
),
);

await widgetTester.pumpAndSettle();

expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2));

final editableTabBar = find.byType(EditableTabBar);

expect(editableTabBar, findsOneWidget);

editableTabBarWidget() =>
(editableTabBar.evaluate().first.widget as EditableTabBar);

expect(editableTabBarWidget().currentIndex, 0);

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.control);
await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.shift);

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.tab);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.tab);
await widgetTester.pumpAndSettle();

expect(editableTabBarWidget().currentIndex, 1,
reason: 'Tab index should roll over');

await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.shift);

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.tab);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.tab);
await widgetTester.pumpAndSettle();

expect(editableTabBarWidget().currentIndex, 0,
reason: 'Tab index should roll back over to 0');

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.tab);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.tab);
await widgetTester.pumpAndSettle();

expect(editableTabBarWidget().currentIndex, 1,
reason: 'Tab index should increase to 1 (no rollover)');

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.tab);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.tab);
await widgetTester.pumpAndSettle();

expect(editableTabBarWidget().currentIndex, 0);
});

testWidgets('Navigate to specific tabs', (widgetTester) async {
FlutterError.onError = ignoreOverflowErrors;

await widgetTester.pumpWidget(
MaterialApp(
home: DashboardPage(
ntConnection: createMockOfflineNT4(),
preferences: preferences,
version: '0.0.0.0',
),
),
);

await widgetTester.pumpAndSettle();

expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2));

final editableTabBar = find.byType(EditableTabBar);

expect(editableTabBar, findsOneWidget);

editableTabBarWidget() =>
(editableTabBar.evaluate().first.widget as EditableTabBar);

expect(editableTabBarWidget().currentIndex, 0);

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.control);

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.digit1);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.digit1);
await widgetTester.pumpAndSettle();

expect(editableTabBarWidget().currentIndex, 0,
reason: 'Tab index should remain at 0');

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.digit2);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.digit2);
await widgetTester.pumpAndSettle();

expect(editableTabBarWidget().currentIndex, 1);

await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.digit5);
await widgetTester.sendKeyUpEvent(LogicalKeyboardKey.digit5);
await widgetTester.pumpAndSettle();

expect(editableTabBarWidget().currentIndex, 1,
reason:
'Tab index should remain at 1 since there is no tab at index 4');
});

testWidgets('Renaming tab', (widgetTester) async {
FlutterError.onError = ignoreOverflowErrors;

Expand Down

0 comments on commit c36cb6d

Please sign in to comment.