From c36cb6d85e59180e9759628c4fe033b3c925a01e Mon Sep 17 00:00:00 2001 From: Gold87 <91761103+Gold872@users.noreply.github.com> Date: Fri, 19 Jul 2024 16:29:10 -0400 Subject: [PATCH] Tab left+right navigation shortcuts (#70) Adds shortcuts for Ctrl + Tab and Ctrl + Shift + Tab to navigate tabs left and right --- lib/pages/dashboard_page.dart | 48 ++++++ lib/services/hotkey_manager.dart | 8 + test/pages/dashboard_page_test.dart | 258 ++++++++++++++++++++++++++++ 3 files changed, 314 insertions(+) diff --git a/lib/pages/dashboard_page.dart b/lib/pages/dashboard_page.dart index 2e0a539c..422a6db2 100644 --- a/lib/pages/dashboard_page.dart +++ b/lib/pages/dashboard_page.dart @@ -746,6 +746,30 @@ class _DashboardPageState extends State 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( @@ -1287,6 +1311,30 @@ class _DashboardPageState extends State 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; diff --git a/lib/services/hotkey_manager.dart b/lib/services/hotkey_manager.dart index 27338947..e745f6dc 100644 --- a/lib/services/hotkey_manager.dart +++ b/lib/services/hotkey_manager.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:collection/collection.dart'; @@ -45,6 +46,13 @@ class HotKeyManager { _initialized = true; } + @visibleForTesting + void tearDown() { + _initialized = false; + _hotKeyList.clear(); + _callbackMap.clear(); + } + int _getNumberModifiersPressed() { int count = 0; diff --git a/test/pages/dashboard_page_test.dart b/test/pages/dashboard_page_test.dart index 7f331c3f..a4e1bc38 100644 --- a/test/pages/dashboard_page_test.dart +++ b/test/pages/dashboard_page_test.dart @@ -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'; @@ -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'; @@ -53,6 +55,10 @@ void main() { preferences = await SharedPreferences.getInstance(); }); + tearDown(() { + hotKeyManager.tearDown(); + }); + testWidgets('Dashboard page loading offline', (widgetTester) async { FlutterError.onError = ignoreOverflowErrors; @@ -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(); @@ -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; @@ -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; @@ -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;