diff --git a/lib/components/toolbar/toolbar.dart b/lib/components/toolbar/toolbar.dart index d7179df01..5e4b77a81 100644 --- a/lib/components/toolbar/toolbar.dart +++ b/lib/components/toolbar/toolbar.dart @@ -107,6 +107,8 @@ class _ToolbarState extends State { Keybinding? _ctrlShiftS; Keybinding? _f11; Keybinding? _ctrlV; + Keybinding? _pageUp; + Keybinding? _pageDown; void _assignKeybindings() { _ctrlF = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyF)], inclusive: true); @@ -120,6 +122,10 @@ class _ToolbarState extends State { _f11 = Keybinding([KeyCode.from(LogicalKeyboardKey.f11)], inclusive: true); _ctrlV = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyV)], inclusive: true); + _pageUp = + Keybinding([KeyCode.from(LogicalKeyboardKey.pageUp)], inclusive: true); + _pageDown = Keybinding([KeyCode.from(LogicalKeyboardKey.pageDown)], + inclusive: true); Keybinder.bind(_ctrlF!, widget.toggleFingerDrawing); Keybinder.bind(_ctrlE!, toggleEraser); @@ -127,6 +133,8 @@ class _ToolbarState extends State { Keybinder.bind(_ctrlShiftS!, toggleExportBar); Keybinder.bind(_f11!, toggleFullscreen); Keybinder.bind(_ctrlV!, widget.paste); + Keybinder.bind(_pageUp!, togglePrimarySecondaryColor); + Keybinder.bind(_pageDown!, toggleEraser); } void _removeKeybindings() { @@ -136,6 +144,8 @@ class _ToolbarState extends State { if (_ctrlShiftS != null) Keybinder.remove(_ctrlShiftS!); if (_f11 != null) Keybinder.remove(_f11!); if (_ctrlV != null) Keybinder.remove(_ctrlV!); + if (_pageUp != null) Keybinder.remove(_pageUp!); + if (_pageDown != null) Keybinder.remove(_pageDown!); } void toggleEraser() { @@ -147,6 +157,38 @@ class _ToolbarState extends State { showColorOptions.value = !showColorOptions.value; } + void togglePrimarySecondaryColor() { + toolOptionsType.value = ToolOptions.hide; + widget.setTool(Pen.currentPen); + + if (Prefs.pinnedColors.value.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('No pinned colors available!'), + )); + return; + } + + if (Prefs.pinnedColors.value.length < 2) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: + Text('Pin at least two colors to toggle primary and secondary!'), + )); + return; + } + + var primaryColorString = Prefs.pinnedColors.value[0]; + var secondaryColorString = Prefs.pinnedColors.value[1]; + + var primary = Color(int.parse(primaryColorString)); + var secondary = Color(int.parse(secondaryColorString)); + + if (Pen.currentPen.color != primary) { + widget.setColor(primary); + } else { + widget.setColor(secondary); + } + } + void toggleExportBar() { showExportOptions.value = !showExportOptions.value; } diff --git a/lib/pages/editor/editor.dart b/lib/pages/editor/editor.dart index faca2a3ec..5099fd199 100644 --- a/lib/pages/editor/editor.dart +++ b/lib/pages/editor/editor.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_quill/flutter_quill.dart' as flutter_quill; +import 'package:focus_detector/focus_detector.dart'; import 'package:keybinder/keybinder.dart'; import 'package:logging/logging.dart'; import 'package:printing/printing.dart'; @@ -1555,96 +1556,106 @@ class EditorState extends State { child: child!, ); }, - child: Scaffold( - appBar: DynamicMaterialApp.isFullscreen - ? null - : AppBar( - toolbarHeight: kToolbarHeight, - title: widget.customTitle != null - ? Text(widget.customTitle!) - : Form( - key: _filenameFormKey, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: TextFormField( - decoration: const InputDecoration( - border: InputBorder.none, + child: FocusDetector( + onFocusGained: () { + // Key bindings won't work until keyboard is shown at least once for some reason. + // This is a workaround to fix that. It should be unnoticable to the user. + if (Platform.isAndroid) { + SystemChannels.textInput.invokeMethod('TextInput.show'); + SystemChannels.textInput.invokeMethod('TextInput.hide'); + } + }, + child: Scaffold( + appBar: DynamicMaterialApp.isFullscreen + ? null + : AppBar( + toolbarHeight: kToolbarHeight, + title: widget.customTitle != null + ? Text(widget.customTitle!) + : Form( + key: _filenameFormKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: TextFormField( + decoration: const InputDecoration( + border: InputBorder.none, + ), + controller: filenameTextEditingController, + onChanged: renameFile, + autofocus: needsNaming, + validator: _validateFilenameTextField, ), - controller: filenameTextEditingController, - onChanged: renameFile, - autofocus: needsNaming, - validator: _validateFilenameTextField, ), - ), - leading: SaveIndicator( - savingState: savingState, - triggerSave: saveToFile, - ), - actions: [ - IconButton( - icon: const AdaptiveIcon( - icon: Icons.insert_page_break, - cupertinoIcon: CupertinoIcons.add, - ), - tooltip: t.editor.menu.insertPage, - onPressed: () => setState(() { - final currentPageIndex = this.currentPageIndex; - insertPageAfter(currentPageIndex); - CanvasGestureDetector.scrollToPage( - pageIndex: currentPageIndex + 1, - pages: coreInfo.pages, - screenWidth: MediaQuery.sizeOf(context).width, - transformationController: _transformationController, - ); - }), + leading: SaveIndicator( + savingState: savingState, + triggerSave: saveToFile, ), - IconButton( - icon: const AdaptiveIcon( - icon: Icons.grid_view, - cupertinoIcon: CupertinoIcons.rectangle_grid_2x2, + actions: [ + IconButton( + icon: const AdaptiveIcon( + icon: Icons.insert_page_break, + cupertinoIcon: CupertinoIcons.add, + ), + tooltip: t.editor.menu.insertPage, + onPressed: () => setState(() { + final currentPageIndex = this.currentPageIndex; + insertPageAfter(currentPageIndex); + CanvasGestureDetector.scrollToPage( + pageIndex: currentPageIndex + 1, + pages: coreInfo.pages, + screenWidth: MediaQuery.sizeOf(context).width, + transformationController: _transformationController, + ); + }), ), - tooltip: t.editor.pages, - onPressed: () { - showDialog( - context: context, - builder: (context) => AdaptiveAlertDialog( - title: Text(t.editor.pages), - content: pageManager(context), - actions: const [], - ), - ); - }, - ), - IconButton( - icon: const AdaptiveIcon( - icon: Icons.more_vert, - cupertinoIcon: CupertinoIcons.ellipsis_vertical, + IconButton( + icon: const AdaptiveIcon( + icon: Icons.grid_view, + cupertinoIcon: CupertinoIcons.rectangle_grid_2x2, + ), + tooltip: t.editor.pages, + onPressed: () { + showDialog( + context: context, + builder: (context) => AdaptiveAlertDialog( + title: Text(t.editor.pages), + content: pageManager(context), + actions: const [], + ), + ); + }, ), - onPressed: () { - showModalBottomSheet( - context: context, - builder: (context) => bottomSheet(context), - isScrollControlled: true, - showDragHandle: true, - backgroundColor: colorScheme.surface, - constraints: const BoxConstraints( - maxWidth: 500, - ), - ); - }, - ) - ], - ), - body: body, - floatingActionButton: (DynamicMaterialApp.isFullscreen && - !Prefs.editorToolbarShowInFullscreen.value) - ? FloatingActionButton( - shape: cupertino ? const CircleBorder() : null, - onPressed: () { - DynamicMaterialApp.setFullscreen(false, updateSystem: true); - }, - child: const Icon(Icons.fullscreen_exit), - ) - : null, + IconButton( + icon: const AdaptiveIcon( + icon: Icons.more_vert, + cupertinoIcon: CupertinoIcons.ellipsis_vertical, + ), + onPressed: () { + showModalBottomSheet( + context: context, + builder: (context) => bottomSheet(context), + isScrollControlled: true, + showDragHandle: true, + backgroundColor: colorScheme.surface, + constraints: const BoxConstraints( + maxWidth: 500, + ), + ); + }, + ) + ], + ), + body: body, + floatingActionButton: (DynamicMaterialApp.isFullscreen && + !Prefs.editorToolbarShowInFullscreen.value) + ? FloatingActionButton( + shape: cupertino ? const CircleBorder() : null, + onPressed: () { + DynamicMaterialApp.setFullscreen(false, updateSystem: true); + }, + child: const Icon(Icons.fullscreen_exit), + ) + : null, + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index f4a260759..e0e27ecdc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -531,6 +531,14 @@ packages: description: flutter source: sdk version: "0.0.0" + focus_detector: + dependency: "direct main" + description: + name: focus_detector + sha256: "05e32d9dd378cd54f1a3f9ce813c05156f28eb83f8e68f5bf1a37e9cdb21af1c" + url: "https://pub.dev" + source: hosted + version: "2.0.1" font_awesome_flutter: dependency: "direct main" description: @@ -1431,6 +1439,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + visibility_detector: + dependency: "direct main" + description: + name: visibility_detector + sha256: ec932527913f32f65aa01d3a393504240b9e9021ecc77123f017755605e48832 + url: "https://pub.dev" + source: hosted + version: "0.2.2" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bd38d3901..6f9b8718c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -138,6 +138,10 @@ dependencies: one_dollar_unistroke_recognizer: ^1.0.0 + focus_detector: ^2.0.1 + + visibility_detector: ^0.2.2 + meta: ^1.0.0 mutex: ^3.1.0 diff --git a/test/editor_undo_redo_test.dart b/test/editor_undo_redo_test.dart index df1938021..d4ae9d568 100644 --- a/test/editor_undo_redo_test.dart +++ b/test/editor_undo_redo_test.dart @@ -9,6 +9,7 @@ import 'package:saber/data/flavor_config.dart'; import 'package:saber/data/prefs.dart'; import 'package:saber/i18n/strings.g.dart'; import 'package:saber/pages/editor/editor.dart'; +import 'package:visibility_detector/visibility_detector.dart'; import 'utils/test_mock_channel_handlers.dart'; @@ -77,6 +78,9 @@ void main() { // draw something await drawOnEditor(tester); + + // visibility detector triggers error withour this + VisibilityDetectorController.instance.updateInterval = Duration.zero; await tester.pumpAndSettle(); expect(getUndoBtn().onPressed, isNotNull, reason: 'Undo button should be enabled after first draw');