Revert "dont allow focus on buttons"

This reverts commit dbe43897b6.
This commit is contained in:
Fabian Freund
2026-01-14 05:22:10 +01:00
parent 057d14b6d9
commit 53df72d906
5 changed files with 320 additions and 424 deletions
@@ -57,7 +57,6 @@ import 'package:weblibre/features/user/domain/repositories/general_settings.dart
import 'package:weblibre/presentation/hooks/cached_future.dart';
import 'package:weblibre/presentation/hooks/menu_controller.dart';
import 'package:weblibre/presentation/icons/tor_icons.dart';
import 'package:weblibre/presentation/widgets/non_focusable.dart';
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart';
@@ -326,90 +325,78 @@ class BrowserTabBar extends HookConsumerWidget {
return Visibility(
visible: tabBarReaderView || readerabilityStateActive,
child: NonFocusable(
child: ReaderButton(
buttonBuilder: (isLoading, readerActive, icon) =>
InkWell(
onTap: isLoading
? null
: () async {
await ref
.read(
readerableScreenControllerProvider
.notifier,
)
.toggleReaderView(
!readerActive,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 15.0,
horizontal: 8.0,
),
child: icon,
child: ReaderButton(
buttonBuilder: (isLoading, readerActive, icon) =>
InkWell(
onTap: isLoading
? null
: () async {
await ref
.read(
readerableScreenControllerProvider
.notifier,
)
.toggleReaderView(!readerActive);
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 15.0,
horizontal: 8.0,
),
child: icon,
),
),
),
),
);
},
),
if (showExtensionShortcut)
NonFocusable(
child: ExtensionShortcutMenu(
controller: extensionMenuController,
child: IconButton(
onPressed: () {
if (extensionMenuController.isOpen) {
extensionMenuController.close();
} else {
extensionMenuController.open();
}
},
icon: const Icon(MdiIcons.puzzle),
),
ExtensionShortcutMenu(
controller: extensionMenuController,
child: IconButton(
onPressed: () {
if (extensionMenuController.isOpen) {
extensionMenuController.close();
} else {
extensionMenuController.open();
}
},
icon: const Icon(MdiIcons.puzzle),
),
),
if (selectedTabId != null)
NonFocusable(
child: TabMenu(
controller: trippleDotMenuController,
selectedTabId: selectedTabId,
builder: (context, controller, child) {
return InkWell(
onTap: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 15.0,
),
child: Icon(MdiIcons.dotsVertical),
TabMenu(
controller: trippleDotMenuController,
selectedTabId: selectedTabId,
builder: (context, controller, child) {
return InkWell(
onTap: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 15.0,
),
);
},
),
child: Icon(MdiIcons.dotsVertical),
),
);
},
),
if (showMainToolbarTabsCount)
NonFocusable(
child: TabsCountButton(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
showLongPressMenu: true,
),
TabsCountButton(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
showLongPressMenu: true,
),
if (showMainToolbarNavigationButton)
NonFocusable(
child: NavigationMenuButton(
selectedTabId: selectedTabId,
showNavigationButtons: true,
),
NavigationMenuButton(
selectedTabId: selectedTabId,
showNavigationButtons: true,
),
],
),
@@ -453,42 +440,32 @@ class ContextualToolbar extends HookConsumerWidget {
children: [
if (tabState?.historyState.canGoBack == true ||
tabState?.isLoading == true)
NonFocusable(
child: NavigateBackButton(
selectedTabId: selectedTabId,
isLoading: tabState?.isLoading ?? false,
),
NavigateBackButton(
selectedTabId: selectedTabId,
isLoading: tabState?.isLoading ?? false,
)
else
NonFocusable(
child: IconButton(
onPressed: () async {
await BookmarkListRoute(
entryGuid: BookmarkRoot.root.id,
).push(context);
},
icon: const Icon(MdiIcons.bookmarkMultiple),
),
IconButton(
onPressed: () async {
await BookmarkListRoute(
entryGuid: BookmarkRoot.root.id,
).push(context);
},
icon: const Icon(MdiIcons.bookmarkMultiple),
),
if (tabState?.historyState.canGoForward == true)
NonFocusable(
child: NavigateForwardButton(selectedTabId: selectedTabId),
)
NavigateForwardButton(selectedTabId: selectedTabId)
else
NonFocusable(child: ShareMenuButton(selectedTabId: selectedTabId)),
const NonFocusable(child: AddTabButton()),
NonFocusable(
child: TabsCountButton(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
showLongPressMenu: false,
),
ShareMenuButton(selectedTabId: selectedTabId),
const AddTabButton(),
TabsCountButton(
selectedTabId: selectedTabId,
displayedSheet: displayedSheet,
showLongPressMenu: false,
),
NonFocusable(
child: NavigationMenuButton(
selectedTabId: selectedTabId,
showNavigationButtons: false,
),
NavigationMenuButton(
selectedTabId: selectedTabId,
showNavigationButtons: false,
),
],
);
@@ -651,17 +628,15 @@ class ShareMenuButton extends HookConsumerWidget {
ShowQrCodeMenuItemButton(selectedTabId: selectedTabId),
],
builder: (context, controller, child) {
return NonFocusable(
child: IconButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
icon: const Icon(Icons.share),
),
return IconButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
icon: const Icon(Icons.share),
);
},
);
@@ -730,19 +705,17 @@ class NavigationMenuButton extends HookConsumerWidget {
alignment: WrapAlignment.center,
children: [
...pageExtensions.map(
(extension) => NonFocusable(
child: IconButton(
onPressed: () async {
//Use parents .ref because after onPressed this consumer gets disposed already
await addonService.invokeAddonAction(
extension.extensionId,
WebExtensionActionType.page,
);
},
icon: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: ExtensionBadgeIcon(extension),
),
(extension) => IconButton(
onPressed: () async {
//Use parents .ref because after onPressed this consumer gets disposed already
await addonService.invokeAddonAction(
extension.extensionId,
WebExtensionActionType.page,
);
},
icon: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: ExtensionBadgeIcon(extension),
),
),
),
@@ -974,20 +947,18 @@ class NavigateForwardButton extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return NonFocusable(
child: IconButton(
onPressed: canGoForward
? () async {
final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
return IconButton(
onPressed: canGoForward
? () async {
final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
await controller.goForward();
menuControllerToClose?.close();
}
: null,
icon: const Icon(Icons.arrow_forward),
),
await controller.goForward();
menuControllerToClose?.close();
}
: null,
icon: const Icon(Icons.arrow_forward),
);
}
}
@@ -1008,37 +979,33 @@ class NavigateBackButton extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return NonFocusable(
child: IconButton(
onPressed: (canGoBack || isLoading)
? () async {
final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
return IconButton(
onPressed: (canGoBack || isLoading)
? () async {
final controller = ref.read(
tabSessionProvider(tabId: selectedTabId).notifier,
);
final isReaderActive = ref.read(
selectedTabStateProvider.select(
(state) => state?.readerableState.active ?? false,
),
);
final isReaderActive = ref.read(
selectedTabStateProvider.select(
(state) => state?.readerableState.active ?? false,
),
);
if (isLoading) {
await controller.stopLoading();
} else if (isReaderActive) {
await ref
.read(readerableScreenControllerProvider.notifier)
.toggleReaderView(false);
} else {
await controller.goBack();
}
menuControllerToClose?.close();
if (isLoading) {
await controller.stopLoading();
} else if (isReaderActive) {
await ref
.read(readerableScreenControllerProvider.notifier)
.toggleReaderView(false);
} else {
await controller.goBack();
}
: null,
icon: isLoading
? const Icon(Icons.close)
: const Icon(Icons.arrow_back),
),
menuControllerToClose?.close();
}
: null,
icon: isLoading ? const Icon(Icons.close) : const Icon(Icons.arrow_back),
);
}
}
@@ -1052,30 +1019,28 @@ class AddTabButton extends HookConsumerWidget {
return TabCreationMenu(
controller: tabMenuController,
child: NonFocusable(
child: IconButton(
onPressed: () async {
final settings = ref.read(generalSettingsWithDefaultsProvider);
child: IconButton(
onPressed: () async {
final settings = ref.read(generalSettingsWithDefaultsProvider);
await SearchRoute(
tabType:
ref.read(selectedTabTypeProvider) ??
settings.defaultCreateTabType,
).push(context);
await SearchRoute(
tabType:
ref.read(selectedTabTypeProvider) ??
settings.defaultCreateTabType,
).push(context);
if (context.mounted) {
const BrowserRoute().go(context);
}
},
icon: const Icon(MdiIcons.tabPlus),
onLongPress: () {
if (tabMenuController.isOpen) {
tabMenuController.close();
} else {
tabMenuController.open();
}
},
),
if (context.mounted) {
const BrowserRoute().go(context);
}
},
icon: const Icon(MdiIcons.tabPlus),
onLongPress: () {
if (tabMenuController.isOpen) {
tabMenuController.close();
} else {
tabMenuController.open();
}
},
),
);
}
@@ -152,24 +152,22 @@ class GridTabPreview extends HookConsumerWidget {
child: Text('Close all from ${tabState.url.host}'),
),
],
child: NonFocusable(
child: IconButton(
visualDensity: const VisualDensity(
horizontal: -4.0,
vertical: -4.0,
),
onPressed: onDelete,
onLongPress: onDeleteAll != null
? () {
if (extendedDeleteMenuController.isOpen) {
extendedDeleteMenuController.close();
} else {
extendedDeleteMenuController.open();
}
}
: null,
icon: const Icon(Icons.close),
child: IconButton(
visualDensity: const VisualDensity(
horizontal: -4.0,
vertical: -4.0,
),
onPressed: onDelete,
onLongPress: onDeleteAll != null
? () {
if (extendedDeleteMenuController.isOpen) {
extendedDeleteMenuController.close();
} else {
extendedDeleteMenuController.open();
}
}
: null,
icon: const Icon(Icons.close),
),
),
?trailingChild,
@@ -352,20 +350,18 @@ class ListTabPreview extends HookConsumerWidget {
child: Text('Close all from ${tabState.url.host}'),
),
],
child: NonFocusable(
child: IconButton(
onPressed: onDelete,
onLongPress: onDeleteAll != null
? () {
if (extendedDeleteMenuController.isOpen) {
extendedDeleteMenuController.close();
} else {
extendedDeleteMenuController.open();
}
child: IconButton(
onPressed: onDelete,
onLongPress: onDeleteAll != null
? () {
if (extendedDeleteMenuController.isOpen) {
extendedDeleteMenuController.close();
} else {
extendedDeleteMenuController.open();
}
: null,
icon: const Icon(Icons.close),
),
}
: null,
icon: const Icon(Icons.close),
),
),
?trailingChild,
@@ -631,12 +627,10 @@ class SuggestedSingleGridTabPreview extends StatelessWidget {
tabId: tabId,
isActive: tabId == activeTabId,
onTap: onTap,
trailingChild: const NonFocusable(
child: IconButton(
visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0),
icon: Icon(MdiIcons.creation),
onPressed: null,
),
trailingChild: const IconButton(
visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0),
icon: Icon(MdiIcons.creation),
onPressed: null,
),
),
);
@@ -665,12 +659,10 @@ class SuggestedSingleListTabPreview extends StatelessWidget {
tabId: tabId,
isActive: tabId == activeTabId,
onTap: onTap,
trailingChild: const NonFocusable(
child: IconButton(
visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0),
icon: Icon(MdiIcons.creation),
onPressed: null,
),
trailingChild: const IconButton(
visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0),
icon: Icon(MdiIcons.creation),
onPressed: null,
),
),
);
@@ -26,7 +26,6 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/c
import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/menu_controller.dart';
import 'package:weblibre/presentation/widgets/non_focusable.dart';
import 'package:weblibre/presentation/widgets/speech_to_text_button.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
@@ -83,31 +82,27 @@ class TabViewHeader extends HookConsumerWidget {
Row(
mainAxisSize: MainAxisSize.min,
children: [
NonFocusable(
child: IconButton(
icon: const Icon(MdiIcons.tabSearch),
iconSize: 18,
padding: EdgeInsets.zero,
tooltip: 'Search inside tabs',
onPressed: () {
switch (ref.read(
tabsViewModeControllerProvider,
)) {
case TabsViewMode.tree:
case TabsViewMode.list:
break;
case TabsViewMode.grid:
ref
.read(
tabsViewModeControllerProvider.notifier,
)
.set(TabsViewMode.list);
}
IconButton(
icon: const Icon(MdiIcons.tabSearch),
iconSize: 18,
padding: EdgeInsets.zero,
tooltip: 'Search inside tabs',
onPressed: () {
switch (ref.read(tabsViewModeControllerProvider)) {
case TabsViewMode.tree:
case TabsViewMode.list:
break;
case TabsViewMode.grid:
ref
.read(
tabsViewModeControllerProvider.notifier,
)
.set(TabsViewMode.list);
}
searchMode.value = true;
searchTextFocus.requestFocus();
},
),
searchMode.value = true;
searchTextFocus.requestFocus();
},
),
const SizedBox(
height: 32,
@@ -131,23 +126,21 @@ class TabViewHeader extends HookConsumerWidget {
),
)
.toList(),
child: NonFocusable(
child: IconButton(
tooltip: 'Change view mode',
onPressed: () {
if (viewModeMenuController.isOpen) {
viewModeMenuController.close();
} else {
viewModeMenuController.open();
}
},
icon: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(tabsViewMode.icon, size: 18),
const Icon(Icons.arrow_drop_down, size: 18),
],
),
child: IconButton(
tooltip: 'Change view mode',
onPressed: () {
if (viewModeMenuController.isOpen) {
viewModeMenuController.close();
} else {
viewModeMenuController.open();
}
},
icon: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(tabsViewMode.icon, size: 18),
const Icon(Icons.arrow_drop_down, size: 18),
],
),
),
),
@@ -174,42 +167,40 @@ class TabViewHeader extends HookConsumerWidget {
style: const TextStyle(fontSize: 10),
)
: null,
child: NonFocusable(
child: IconButton.filledTonal(
icon: const Icon(MdiIcons.imageAutoAdjust),
isSelected: tabSuggestionsEnabled,
iconSize: 18,
padding: EdgeInsets.zero,
tooltip: downloadProgress != null
? 'Downloading AI models (${downloadProgress.progress.toInt()}%)'
: tabSuggestionsEnabled
? 'Disable AI tab suggestions'
: 'Enable AI tab suggestions',
onPressed: () async {
if (!tabSuggestionsEnabled) {
final result =
await showEnableAiTabSuggestionsDialog(
context,
);
child: IconButton.filledTonal(
icon: const Icon(MdiIcons.imageAutoAdjust),
isSelected: tabSuggestionsEnabled,
iconSize: 18,
padding: EdgeInsets.zero,
tooltip: downloadProgress != null
? 'Downloading AI models (${downloadProgress.progress.toInt()}%)'
: tabSuggestionsEnabled
? 'Disable AI tab suggestions'
: 'Enable AI tab suggestions',
onPressed: () async {
if (!tabSuggestionsEnabled) {
final result =
await showEnableAiTabSuggestionsDialog(
context,
);
if (result == true) {
ref
.read(
tabSuggestionsControllerProvider
.notifier,
)
.enable();
}
} else {
if (result == true) {
ref
.read(
tabSuggestionsControllerProvider
.notifier,
)
.disable();
.enable();
}
},
),
} else {
ref
.read(
tabSuggestionsControllerProvider
.notifier,
)
.disable();
}
},
),
);
},
@@ -513,18 +504,16 @@ class TabViewHeader extends HookConsumerWidget {
},
),
],
child: NonFocusable(
child: IconButton(
tooltip: 'Tab actions',
onPressed: () {
if (tabsActionMenuController.isOpen) {
tabsActionMenuController.close();
} else {
tabsActionMenuController.open();
}
},
icon: const Icon(MdiIcons.dotsVertical),
),
child: IconButton(
tooltip: 'Tab actions',
onPressed: () {
if (tabsActionMenuController.isOpen) {
tabsActionMenuController.close();
} else {
tabsActionMenuController.open();
}
},
icon: const Icon(MdiIcons.dotsVertical),
),
),
],
@@ -548,15 +537,13 @@ class TabViewHeader extends HookConsumerWidget {
searchTextController.text = data;
},
),
NonFocusable(
child: IconButton(
onPressed: () {
searchTextController.clear();
searchTextFocus.requestFocus();
searchMode.value = false;
},
icon: const Icon(Icons.clear),
),
IconButton(
onPressed: () {
searchTextController.clear();
searchTextFocus.requestFocus();
searchMode.value = false;
},
icon: const Icon(Icons.clear),
),
],
),
@@ -26,7 +26,6 @@ import 'package:weblibre/features/geckoview/features/tabs/data/entities/containe
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart';
import 'package:weblibre/presentation/widgets/non_focusable.dart';
class TabsActionButton extends HookConsumerWidget {
final bool isActive;
@@ -57,65 +56,63 @@ class TabsActionButton extends HookConsumerWidget {
selectedContainerDataProvider.select((value) => value.value?.color),
);
return NonFocusable(
child: InkWell(
onTap: onTap,
onDoubleTap: onDoubleTap,
onLongPress: onLongPress,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 15.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: 2.0,
color: isActive
? theme.colorScheme.primary
: DefaultTextStyle.of(context).style.color!,
),
borderRadius: BorderRadius.circular(5.0),
color: containerColor.mapNotNull(ContainerColors.forAppBar),
return InkWell(
onTap: onTap,
onDoubleTap: onDoubleTap,
onLongPress: onLongPress,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 15.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: 2.0,
color: isActive
? theme.colorScheme.primary
: DefaultTextStyle.of(context).style.color!,
),
constraints: const BoxConstraints(minWidth: 25.0),
child: Center(
child: tabCount.when(
skipLoadingOnReload: true,
data: (count) {
return Text(
count.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: isActive ? theme.colorScheme.primary : null,
),
);
},
loading: () => (tabCount.hasValue)
? Text(
tabCount.value.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: isActive ? theme.colorScheme.primary : null,
),
)
: const Skeletonizer(child: Text('00')),
error: (error, stackTrace) {
logger.e(
'Could not determine tab count',
error: error,
stackTrace: stackTrace,
);
borderRadius: BorderRadius.circular(5.0),
color: containerColor.mapNotNull(ContainerColors.forAppBar),
),
constraints: const BoxConstraints(minWidth: 25.0),
child: Center(
child: tabCount.when(
skipLoadingOnReload: true,
data: (count) {
return Text(
count.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: isActive ? theme.colorScheme.primary : null,
),
);
},
loading: () => (tabCount.hasValue)
? Text(
tabCount.value.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: isActive ? theme.colorScheme.primary : null,
),
)
: const Skeletonizer(child: Text('00')),
error: (error, stackTrace) {
logger.e(
'Could not determine tab count',
error: error,
stackTrace: stackTrace,
);
return Text(
'-1',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: isActive ? theme.colorScheme.primary : null,
),
);
},
),
return Text(
'-1',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
color: isActive ? theme.colorScheme.primary : null,
),
);
},
),
),
),
@@ -1,45 +0,0 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
/// A wrapper that prevents a widget from receiving keyboard focus.
///
/// This fixes the issue where arrow keys on a bluetooth keyboard navigate
/// to toolbar buttons instead of moving the text cursor in input fields.
///
/// Example:
/// ```dart
/// NonFocusable(
/// child: IconButton(
/// onPressed: () {},
/// icon: Icon(Icons.menu),
/// ),
/// )
/// ```
class NonFocusable extends StatelessWidget {
final Widget child;
const NonFocusable({required this.child, super.key});
@override
Widget build(BuildContext context) {
return Focus(canRequestFocus: false, skipTraversal: true, child: child);
}
}