From ce6ed1b5fbd89ce7c0834d2d321d1e61d0b1d694 Mon Sep 17 00:00:00 2001 From: Airyzz <36567925+Airyzz@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:32:56 +1030 Subject: [PATCH] Add custom bottom padding for onscreen keyboards on desktop --- commet/lib/config/preferences.dart | 4 ++ commet/lib/main.dart | 66 ++++++++++--------- .../lib/ui/molecules/direct_message_list.dart | 1 + .../room_quick_access_menu_desktop.dart | 2 +- .../ui/pages/main/main_page_view_desktop.dart | 2 + .../developer/developer_settings_page.dart | 15 ++++- commet/lib/utils/custom_safe_area.dart | 49 ++++++++++++++ commet/lib/utils/event_bus.dart | 3 + commet/lib/utils/focus_node_monitor.dart | 43 ++++++++++++ 9 files changed, 153 insertions(+), 32 deletions(-) create mode 100644 commet/lib/utils/custom_safe_area.dart create mode 100644 commet/lib/utils/focus_node_monitor.dart diff --git a/commet/lib/config/preferences.dart b/commet/lib/config/preferences.dart index 2e8510091..453ae1780 100644 --- a/commet/lib/config/preferences.dart +++ b/commet/lib/config/preferences.dart @@ -324,6 +324,10 @@ class Preferences { DoublePreference emojiPickerHeight = DoublePreference("emoji_picker_height", defaultValue: 300); + DoublePreference customOnscreenKeyboardViewOffset = DoublePreference( + "custom_onscreen_keyboard_view_offset", + defaultValue: 0.0); + StringPreference proxyUrl = StringPreference("proxy_url", defaultValue: "proxy.commet.chat"); diff --git a/commet/lib/main.dart b/commet/lib/main.dart index 392accb01..0c501f08e 100644 --- a/commet/lib/main.dart +++ b/commet/lib/main.dart @@ -21,12 +21,14 @@ import 'package:commet/ui/pages/login/login_page.dart'; import 'package:commet/ui/pages/main/main_page.dart'; import 'package:commet/ui/pages/setup/menus/check_for_updates.dart'; import 'package:commet/utils/android_intent_helper.dart'; +import 'package:commet/utils/custom_safe_area.dart'; import 'package:commet/utils/custom_uri.dart'; import 'package:commet/utils/background_tasks/background_task_manager.dart'; import 'package:commet/utils/database/database_server.dart'; import 'package:commet/utils/emoji/unicode_emoji.dart'; import 'package:commet/utils/event_bus.dart'; import 'package:commet/utils/first_time_setup.dart'; +import 'package:commet/utils/focus_node_monitor.dart'; import 'package:commet/utils/scaled_app.dart'; import 'package:commet/utils/shortcuts_manager.dart'; import 'package:commet/utils/system_wide_shortcuts/system_wide_shortcuts.dart'; @@ -310,36 +312,40 @@ class App extends StatelessWidget { @override Widget build(BuildContext context) { - return TextScaleChanger( - child: ThemeChanger( - shouldFollowSystemTheme: () => - preferences.shouldFollowSystemTheme.value, - getDarkTheme: () { - return preferences.resolveTheme( - overrideBrightness: Brightness.dark); - }, - getLightTheme: () { - return preferences.resolveTheme( - overrideBrightness: Brightness.light); - }, - initialTheme: initialTheme ?? ThemeDark.theme, - materialAppBuilder: (context, theme) { - return MaterialApp( - title: 'Commet', - theme: theme, - debugShowCheckedModeBanner: false, - navigatorKey: navigator, - builder: (context, child) => Provider( - create: (context) => clientManager, - child: child, - ), - home: AppView( - clientManager: clientManager, - initialClientId: initialClientId, - initialRoom: initialRoom, - ), - ); - }), + return CustomSafeArea( + child: FocusNodeMonitor( + child: TextScaleChanger( + child: ThemeChanger( + shouldFollowSystemTheme: () => + preferences.shouldFollowSystemTheme.value, + getDarkTheme: () { + return preferences.resolveTheme( + overrideBrightness: Brightness.dark); + }, + getLightTheme: () { + return preferences.resolveTheme( + overrideBrightness: Brightness.light); + }, + initialTheme: initialTheme ?? ThemeDark.theme, + materialAppBuilder: (context, theme) { + return MaterialApp( + title: 'Commet', + theme: theme, + debugShowCheckedModeBanner: false, + navigatorKey: navigator, + builder: (context, child) => Provider( + create: (context) => clientManager, + child: child, + ), + home: AppView( + clientManager: clientManager, + initialClientId: initialClientId, + initialRoom: initialRoom, + ), + ); + }), + ), + ), ); } } diff --git a/commet/lib/ui/molecules/direct_message_list.dart b/commet/lib/ui/molecules/direct_message_list.dart index 6b4a6dc91..9f30ef9bd 100644 --- a/commet/lib/ui/molecules/direct_message_list.dart +++ b/commet/lib/ui/molecules/direct_message_list.dart @@ -88,6 +88,7 @@ class _DirectMessageListState extends State { return ImplicitlyAnimatedList( itemData: rooms, initialAnimation: false, + padding: EdgeInsets.zero, itemBuilder: (context, room) { final component = room.client.getComponent(); final id = component?.getDirectMessagePartnerId(room); diff --git a/commet/lib/ui/organisms/room_quick_access_menu/room_quick_access_menu_desktop.dart b/commet/lib/ui/organisms/room_quick_access_menu/room_quick_access_menu_desktop.dart index 57dc39c9d..0a4cb1005 100644 --- a/commet/lib/ui/organisms/room_quick_access_menu/room_quick_access_menu_desktop.dart +++ b/commet/lib/ui/organisms/room_quick_access_menu/room_quick_access_menu_desktop.dart @@ -47,6 +47,6 @@ class _RoomQuickAccessMenuViewDesktopState } void onChanged(event) { - setState(() {}); + if (mounted) setState(() {}); } } diff --git a/commet/lib/ui/pages/main/main_page_view_desktop.dart b/commet/lib/ui/pages/main/main_page_view_desktop.dart index dcb1fb691..ffd97b40e 100644 --- a/commet/lib/ui/pages/main/main_page_view_desktop.dart +++ b/commet/lib/ui/pages/main/main_page_view_desktop.dart @@ -400,6 +400,8 @@ class MainPageViewDesktop extends StatelessWidget { Widget buildRoomPicker(BuildContext context) { if (state.currentSpace == null) { return ScaledSafeArea( + top: true, + bottom: false, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/commet/lib/ui/pages/settings/categories/developer/developer_settings_page.dart b/commet/lib/ui/pages/settings/categories/developer/developer_settings_page.dart index 93a26c993..1e492edbb 100644 --- a/commet/lib/ui/pages/settings/categories/developer/developer_settings_page.dart +++ b/commet/lib/ui/pages/settings/categories/developer/developer_settings_page.dart @@ -9,6 +9,7 @@ import 'package:commet/main.dart'; import 'package:commet/ui/navigation/navigation_utils.dart'; import 'package:commet/ui/pages/developer/benchmarks/timeline_viewer_benchmark.dart'; import 'package:commet/ui/pages/settings/categories/app/boolean_toggle.dart'; +import 'package:commet/ui/pages/settings/categories/app/double_preference_slider.dart'; import 'package:commet/ui/pages/settings/categories/developer/cumulative_diagnostics_widget.dart'; import 'package:commet/utils/background_tasks/background_task_manager.dart'; import 'package:commet/utils/background_tasks/mock_tasks.dart'; @@ -52,7 +53,15 @@ class _DeveloperSettingsPageState extends State { title: "Disable Text Cursor Management", description: "As part of the implementaton for the rich text editor, we sometimes have to make automated changes to the text cursor. This disables that", - ) + ), + DoublePreferenceSlider( + preference: preferences.customOnscreenKeyboardViewOffset, + title: "Keyboard Offset", + min: 0.0, + max: 1000, + description: + "Amount to shift the app view up by when the onscreen keyboard is shown", + ) ], ), ) @@ -145,6 +154,10 @@ class _DeveloperSettingsPageState extends State { text: "1280x720", onTap: () => windowManager.setSize(const Size(1280, 720)), ), + tiamat.Button( + text: "1280x800 (Steamdeck)", + onTap: () => windowManager.setSize(const Size(1280, 800)), + ), tiamat.Button( text: "1920x1080", onTap: () => windowManager.setSize(const Size(1920, 1080)), diff --git a/commet/lib/utils/custom_safe_area.dart b/commet/lib/utils/custom_safe_area.dart new file mode 100644 index 000000000..adc7c855b --- /dev/null +++ b/commet/lib/utils/custom_safe_area.dart @@ -0,0 +1,49 @@ +import 'package:commet/main.dart'; +import 'package:commet/utils/event_bus.dart'; +import 'package:flutter/material.dart'; + +class CustomSafeArea extends StatefulWidget { + const CustomSafeArea({required this.child, super.key}); + final Widget child; + + @override + State createState() => _CustomSafeAreaState(); +} + +class _CustomSafeAreaState extends State { + bool isTextFieldFocused = false; + + @override + void initState() { + EventBus.onTextFieldFocused.stream.listen(onTextFieldFocused); + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (preferences.customOnscreenKeyboardViewOffset.value == 0) { + return widget.child; + } + + var query = MediaQuery.of(context); + + return MediaQuery( + data: query.copyWith( + viewPadding: EdgeInsets.fromLTRB( + 0, + 0, + 0, + isTextFieldFocused + ? preferences.customOnscreenKeyboardViewOffset.value + : 0)), + child: widget.child); + } + + void onTextFieldFocused(bool event) { + if (event != isTextFieldFocused) { + setState(() { + isTextFieldFocused = event; + }); + } + } +} diff --git a/commet/lib/utils/event_bus.dart b/commet/lib/utils/event_bus.dart index 0b17c8f76..7ce9125b8 100644 --- a/commet/lib/utils/event_bus.dart +++ b/commet/lib/utils/event_bus.dart @@ -25,6 +25,9 @@ class EventBus { static StreamController setFilterClient = StreamController.broadcast(); + static StreamController onTextFieldFocused = + StreamController.broadcast(); + /// Called when the user initially logs in to the app, or on app startup when atleast one user account is already logged in static StreamController onLoggedIn = StreamController.broadcast(); diff --git a/commet/lib/utils/focus_node_monitor.dart b/commet/lib/utils/focus_node_monitor.dart new file mode 100644 index 000000000..6130848d1 --- /dev/null +++ b/commet/lib/utils/focus_node_monitor.dart @@ -0,0 +1,43 @@ +import 'package:commet/utils/event_bus.dart'; +import 'package:flutter/material.dart'; + +class FocusNodeMonitor extends StatefulWidget { + const FocusNodeMonitor({required this.child, super.key}); + final Widget child; + + @override + State createState() => _FocusNodeMonitorState(); +} + +class _FocusNodeMonitorState extends State { + @override + void initState() { + FocusManager.instance.addListener(onFocusChanged); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } + + void onFocusChanged() { + var focus = FocusManager.instance.primaryFocus; + bool isText = isEditableTextFocused(focus); + EventBus.onTextFieldFocused.add(isText); + } + + bool isEditableTextFocused(FocusNode? focus) { + if (focus?.context?.widget case Focus widget) { + if (widget.child case NotificationListener listener) { + if (listener.child case Scrollable scrollable) { + if (scrollable.restorationId == "editable") { + return true; + } + } + } + } + + return false; + } +}