import 'dart:io'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:flutter/material.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:provider/provider.dart'; import 'package:scaled_app/scaled_app.dart'; typedef KeyDownHandler = void Function(HotKey hotKey); ValueNotifier switchToTheNextSpace = ValueNotifier(0); ValueNotifier createNewPageNotifier = ValueNotifier(0); /// Helper class that utilizes the global [HotKeyManager] to easily /// add a [HotKey] with different handlers. /// /// Makes registration of a [HotKey] simple and easy to read, and makes /// sure the [KeyDownHandler], and other handlers, are grouped with the /// relevant [HotKey]. /// class HotKeyItem { HotKeyItem({ required this.hotKey, this.keyDownHandler, }); final HotKey hotKey; final KeyDownHandler? keyDownHandler; void register() => hotKeyManager.register(hotKey, keyDownHandler: keyDownHandler); } class HomeHotKeys extends StatefulWidget { const HomeHotKeys({ super.key, required this.userProfile, required this.child, }); final UserProfilePB userProfile; final Widget child; @override State createState() => _HomeHotKeysState(); } class _HomeHotKeysState extends State { final windowSizeManager = WindowSizeManager(); late final items = [ // Collapse sidebar menu (using slash) HotKeyItem( hotKey: HotKey( KeyCode.backslash, modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], scope: HotKeyScope.inapp, ), keyDownHandler: (_) => context .read() .add(const HomeSettingEvent.collapseMenu()), ), // Collapse sidebar menu (using .) HotKeyItem( hotKey: HotKey( KeyCode.period, modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], scope: HotKeyScope.inapp, ), keyDownHandler: (_) => context .read() .add(const HomeSettingEvent.collapseMenu()), ), // Toggle theme mode light/dark HotKeyItem( hotKey: HotKey( KeyCode.keyL, modifiers: [ Platform.isMacOS ? KeyModifier.meta : KeyModifier.control, KeyModifier.shift, ], scope: HotKeyScope.inapp, ), keyDownHandler: (_) => context.read().toggleThemeMode(), ), // Close current tab HotKeyItem( hotKey: HotKey( KeyCode.keyW, modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], scope: HotKeyScope.inapp, ), keyDownHandler: (_) => context.read().add(const TabsEvent.closeCurrentTab()), ), // Go to previous tab HotKeyItem( hotKey: HotKey( KeyCode.pageUp, modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], scope: HotKeyScope.inapp, ), keyDownHandler: (_) => _selectTab(context, -1), ), // Go to next tab HotKeyItem( hotKey: HotKey( KeyCode.pageDown, modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], scope: HotKeyScope.inapp, ), keyDownHandler: (_) => _selectTab(context, 1), ), // Rename current view HotKeyItem( hotKey: HotKey( KeyCode.f2, scope: HotKeyScope.inapp, ), keyDownHandler: (_) => getIt().add(const RenameViewEvent.open()), ), // Scale up/down the app // In some keyboards, the system returns equal as + keycode, while others may return add as + keycode, so add them both as zoom in key. ...[KeyCode.equal, KeyCode.add].map( (keycode) => HotKeyItem( hotKey: HotKey( keycode, modifiers: [ Platform.isMacOS ? KeyModifier.meta : KeyModifier.control, ], scope: HotKeyScope.inapp, ), keyDownHandler: (_) => _scaleWithStep(0.1), ), ), HotKeyItem( hotKey: HotKey( KeyCode.minus, modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], scope: HotKeyScope.inapp, ), keyDownHandler: (_) => _scaleWithStep(-0.1), ), // Reset app scaling HotKeyItem( hotKey: HotKey( KeyCode.digit0, modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], scope: HotKeyScope.inapp, ), keyDownHandler: (_) => _scaleToSize(1), ), // Switch to the next space HotKeyItem( hotKey: HotKey( KeyCode.keyO, modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], scope: HotKeyScope.inapp, ), keyDownHandler: (_) => switchToTheNextSpace.value++, ), // Create a new page HotKeyItem( hotKey: HotKey( KeyCode.keyN, modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], scope: HotKeyScope.inapp, ), keyDownHandler: (_) => createNewPageNotifier.value++, ), // Open settings dialog openSettingsHotKey(context, widget.userProfile), ]; @override void initState() { super.initState(); _registerHotKeys(context); } @override void didChangeDependencies() { super.didChangeDependencies(); _registerHotKeys(context); } @override Widget build(BuildContext context) => widget.child; void _registerHotKeys(BuildContext context) { for (final element in items) { element.register(); } } void _selectTab(BuildContext context, int change) { final bloc = context.read(); bloc.add(TabsEvent.selectTab(bloc.state.currentIndex + change)); } Future _scaleWithStep(double step) async { final currentScaleFactor = await windowSizeManager.getScaleFactor(); final textScale = (currentScaleFactor + step).clamp( WindowSizeManager.minScaleFactor, WindowSizeManager.maxScaleFactor, ); Log.info('scale the app from $currentScaleFactor to $textScale'); await _scaleToSize(textScale); } Future _scaleToSize(double size) async { ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => size; await windowSizeManager.setScaleFactor(size); } }