From ab0ff01deebef7570512d07d2d6b0ffab7354f36 Mon Sep 17 00:00:00 2001 From: Julian Mutter Date: Tue, 30 Sep 2025 08:43:28 +0200 Subject: [PATCH] Add keyboard shortcuts for usage with BT Pedal --- lib/bt_pedal_shortcuts.dart | 42 +++++ lib/sheet_viewer_page.dart | 332 +++++++++++++++++++----------------- 2 files changed, 214 insertions(+), 160 deletions(-) create mode 100644 lib/bt_pedal_shortcuts.dart diff --git a/lib/bt_pedal_shortcuts.dart b/lib/bt_pedal_shortcuts.dart new file mode 100644 index 0000000..8fa154a --- /dev/null +++ b/lib/bt_pedal_shortcuts.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class BtPedalShortcuts extends StatefulWidget { + final Widget child; + final VoidCallback? onTurnPageForward; + final VoidCallback? onTurnPageBackward; + + const BtPedalShortcuts({ + super.key, + required this.child, + this.onTurnPageForward, + this.onTurnPageBackward, + }); + + @override + State createState() => _BtPedalShortcutsState(); +} + +class _BtPedalShortcutsState extends State { + String lastAction = "Press pedal..."; + + @override + Widget build(BuildContext context) { + return CallbackShortcuts( + bindings: { + // Shortcuts for page forward + const SingleActivator(LogicalKeyboardKey.arrowUp): + widget.onTurnPageForward ?? () => {}, + const SingleActivator(LogicalKeyboardKey.arrowRight): + widget.onTurnPageForward ?? () => {}, + + // Shortcuts for page backward + const SingleActivator(LogicalKeyboardKey.arrowDown): + widget.onTurnPageBackward ?? () => {}, + const SingleActivator(LogicalKeyboardKey.arrowLeft): + widget.onTurnPageBackward ?? () => {}, + }, + child: Focus(autofocus: true, child: widget.child), + ); + } +} diff --git a/lib/sheet_viewer_page.dart b/lib/sheet_viewer_page.dart index 013ae7a..f1533f0 100644 --- a/lib/sheet_viewer_page.dart +++ b/lib/sheet_viewer_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_drawing_board/flutter_drawing_board.dart'; import 'package:logging/logging.dart'; import 'package:pdfrx/pdfrx.dart'; import 'package:sheetless/api.dart'; +import 'package:sheetless/bt_pedal_shortcuts.dart'; import 'package:sheetless/sheet.dart'; import 'package:sheetless/storage_helper.dart'; @@ -86,192 +87,203 @@ class _SheetViewerPageState extends State { } } + void turnPage(int numTurns) { + setState(() { + page += numTurns; + page = page.clamp(1, numPages); + }); + } + @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.sheet.name), - actions: [ - IconButton( - onPressed: () { - setState(() { - if (widget.config.twoPageMode) { - // TODO: notification that paint mode only in single page mode - } else { - paintMode = !paintMode; - } - }); - }, - icon: Icon(Icons.brush), - ), - IconButton( - onPressed: () { - setState(() { - widget.config.twoPageMode = !widget.config.twoPageMode; - storageHelper.writeConfig(widget.config); - if (widget.config.twoPageMode) { - paintMode = false; - // TODO: notification that paint mode was deactivated since only possible in single page mode - } - }); - }, - icon: Icon( - widget.config.twoPageMode ? Icons.filter_1 : Icons.filter_2, + return BtPedalShortcuts( + onTurnPageForward: () => turnPage(1), + onTurnPageBackward: () => turnPage(-1), + child: Scaffold( + appBar: AppBar( + title: Text(widget.sheet.name), + actions: [ + IconButton( + onPressed: () { + setState(() { + if (widget.config.twoPageMode) { + // TODO: notification that paint mode only in single page mode + } else { + paintMode = !paintMode; + } + }); + }, + icon: Icon(Icons.brush), ), - ), - ], - ), - body: FutureBuilder( - future: documentLoaded, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData && document != null) { - numPages = document!.pages.length; + IconButton( + onPressed: () { + setState(() { + widget.config.twoPageMode = !widget.config.twoPageMode; + storageHelper.writeConfig(widget.config); + if (widget.config.twoPageMode) { + paintMode = false; + // TODO: notification that paint mode was deactivated since only possible in single page mode + } + }); + }, + icon: Icon( + widget.config.twoPageMode ? Icons.filter_1 : Icons.filter_2, + ), + ), + ], + ), + body: FutureBuilder( + future: documentLoaded, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData && document != null) { + numPages = document!.pages.length; - return Stack( - children: [ - Stack( - children: [ - Visibility( - visible: !paintMode, - child: GestureDetector( - onTapUp: (TapUpDetails details) { - // Get the size of the screen - final screenWidth = MediaQuery.of(context).size.width; + return Stack( + children: [ + Stack( + children: [ + Visibility( + visible: !paintMode, + child: GestureDetector( + onTapUp: (TapUpDetails details) { + // Get the size of the screen + final screenWidth = MediaQuery.of( + context, + ).size.width; - // print("Touch at y = ${details.localPosition.dy}"); - // print("Touch at x = ${details.localPosition.dx}"); - // print("Screenwidth = ${screenWidth}"); - // Check where the user tapped - // if (details.localPosition.dy < 100) { - // TODO - // setState(() { - // toggleFullscreen(); - // }); - if (details.localPosition.dx < screenWidth / 2) { - // Left half of the screen - setState(() { - page = page > 1 ? page - 1 : 1; - }); - } else { - // Right half of the screen - setState(() { - page = page < numPages ? page + 1 : numPages; - }); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 0, - children: [ - Expanded( - child: Stack( - children: [ - PdfPageView( - key: ValueKey(page), - document: document, - pageNumber: page, - maximumDpi: 300, - alignment: widget.config.twoPageMode - ? Alignment.centerRight - : Alignment.center, - ), - Positioned.fill( - child: Container( - alignment: Alignment.bottomCenter, - padding: EdgeInsets.only(bottom: 5), - child: Text('$page / $numPages'), - ), - ), - ], - ), - ), - Visibility( - visible: widget.config.twoPageMode == true, - child: Expanded( + // print("Touch at y = ${details.localPosition.dy}"); + // print("Touch at x = ${details.localPosition.dx}"); + // print("Screenwidth = ${screenWidth}"); + // Check where the user tapped + // if (details.localPosition.dy < 100) { + // TODO + // setState(() { + // toggleFullscreen(); + // }); + if (details.localPosition.dx < screenWidth / 2) { + // Left half of the screen + turnPage(-1); + } else { + // Right half of the screen + turnPage(1); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 0, + children: [ + Expanded( child: Stack( children: [ PdfPageView( key: ValueKey(page), document: document, - pageNumber: page + 1, + pageNumber: page, maximumDpi: 300, - alignment: Alignment.centerLeft, - // alignment: Alignment.center, + alignment: widget.config.twoPageMode + ? Alignment.centerRight + : Alignment.center, ), Positioned.fill( child: Container( alignment: Alignment.bottomCenter, padding: EdgeInsets.only(bottom: 5), - child: Text('${page + 1} / $numPages'), + child: Text('$page / $numPages'), ), ), ], ), ), - ), - ], - ), - ), - ), - - // Positioned.fill( - Visibility( - visible: paintMode, - child: SizedBox.expand( - child: LayoutBuilder( - builder: (context, constraints) { - final maxSize = Size( - constraints.maxWidth, - constraints.maxHeight, - ); - final pageSizeUnscaled = document!.pages - .elementAt(page) - .size; - final pageSizeScaled = calcScaledPageSize( - maxSize, - pageSizeUnscaled, - ); - return DrawingBoard( - background: SizedBox( - width: pageSizeScaled.width, - height: pageSizeScaled.height, - child: PdfPageView( - document: document, - pageNumber: page, - alignment: Alignment.center, + Visibility( + visible: widget.config.twoPageMode == true, + child: Expanded( + child: Stack( + children: [ + PdfPageView( + key: ValueKey(page), + document: document, + pageNumber: page + 1, + maximumDpi: 300, + alignment: Alignment.centerLeft, + // alignment: Alignment.center, + ), + Positioned.fill( + child: Container( + alignment: Alignment.bottomCenter, + padding: EdgeInsets.only(bottom: 5), + child: Text( + '${page + 1} / $numPages', + ), + ), + ), + ], + ), ), ), - // showDefaultTools: true, - // showDefaultActions: true, - boardConstrained: true, - minScale: 1, - maxScale: 3, - alignment: Alignment.topRight, - boardBoundaryMargin: EdgeInsets.all(0), - ); - }, + ], + ), ), ), - ), - ], + + // Positioned.fill( + Visibility( + visible: paintMode, + child: SizedBox.expand( + child: LayoutBuilder( + builder: (context, constraints) { + final maxSize = Size( + constraints.maxWidth, + constraints.maxHeight, + ); + final pageSizeUnscaled = document!.pages + .elementAt(page) + .size; + final pageSizeScaled = calcScaledPageSize( + maxSize, + pageSizeUnscaled, + ); + return DrawingBoard( + background: SizedBox( + width: pageSizeScaled.width, + height: pageSizeScaled.height, + child: PdfPageView( + document: document, + pageNumber: page, + alignment: Alignment.center, + ), + ), + // showDefaultTools: true, + // showDefaultActions: true, + boardConstrained: true, + minScale: 1, + maxScale: 3, + alignment: Alignment.topRight, + boardBoundaryMargin: EdgeInsets.all(0), + ); + }, + ), + ), + ), + ], + ), + ], + ); + } else if (snapshot.hasError) { + return Center( + child: Text( + style: Theme.of( + context, + ).textTheme.displaySmall!.copyWith(color: Colors.red), + textAlign: TextAlign.center, + snapshot.error.toString(), ), - ], - ); - } else if (snapshot.hasError) { - return Center( - child: Text( - style: Theme.of( - context, - ).textTheme.displaySmall!.copyWith(color: Colors.red), - textAlign: TextAlign.center, - snapshot.error.toString(), - ), - ); - } else { - return const Center(child: CircularProgressIndicator()); - } - }, + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), ), ); }