Space never presses a focused button; chat toasts open the chat; plain wheel always scrolls the page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-09 07:52:48 +03:00
1 parent cf80bb2167
commit f2272d7980
8 files changed
+56 -21

No files matched your search

@@ -501,7 +501,6 @@ class _SchedulePageState extends State<SchedulePage> {
(byDay[DateTime(l.year, l.month, l.day)] ??= []).add(e);
}
return HScroll(
wheel: false,
controller: _scroll,
builder: (c) => ListView.separated(
key: const ValueKey('schedule-board'),
+5 -1
View File
@@ -669,7 +669,11 @@ class AppShellState extends State<AppShell> with SingleTickerProviderStateMixin
),
),
if (online && _shown != PlayerMode.none) _playerLayer(area),
Positioned(left: 16, bottom: _fullscreen ? 90 : 16, child: const PartyToasts()),
Positioned(left: 16, bottom: _fullscreen ? 90 : 16, child: PartyToasts(
onOpenChat: () {
if (!_chatOpen) _toggleChat();
},
)),
],
);
});
+7 -7
View File
@@ -90,17 +90,17 @@ Future<T?> showPopover<T>(
},
);
/// Horizontal lists for mouse users: drag with the mouse, and the plain
/// (vertical) wheel moves them sideways. At either end the wheel is left
/// to the page, so a row never traps vertical scrolling.
/// Horizontal lists for mouse users: drag with the mouse or Shift+wheel;
/// the plain wheel stays vertical (the page scrolls under the cursor, not
/// the row).
class HScroll extends StatefulWidget {
final ScrollController? controller;
final Widget Function(ScrollController controller) builder;
/// Map the plain (vertical) wheel to sideways scrolling. Off where the
/// children scroll vertically themselves (the schedule's day columns):
/// there only mouse drag and Shift+wheel move sideways.
/// Also map the plain (vertical) wheel to sideways scrolling (at either
/// end the wheel goes to the page). Off by default: one wheel doing two
/// things was confusing.
final bool wheel;
const HScroll({super.key, this.controller, required this.builder, this.wheel = true});
const HScroll({super.key, this.controller, required this.builder, this.wheel = false});
@override
State<HScroll> createState() => _HScrollState();
@@ -92,6 +92,7 @@ class PartyStrip extends StatelessWidget {
message: 'Your profile',
child: InkWell(
key: const ValueKey('strip-profile'),
canRequestFocus: false,
customBorder: const CircleBorder(),
onTap: onProfile,
child: StatusAvatar(
+32 -5
View File
@@ -43,10 +43,13 @@ ShellToast shellToastFor(ToastItem t, AvatarInfo Function(String name) avatar) {
}
/// The coordinator's toasts as cards with avatars, bottom-left: each
/// slides in, stays [life], then slides out and collapses its gap.
/// slides in, stays [life], then slides out and collapses its gap. A click
/// on a chat toast opens the chat ([onOpenChat]); the rest let clicks
/// through.
class PartyToasts extends StatefulWidget {
final Duration life;
const PartyToasts({super.key, this.life = const Duration(seconds: 5)});
final VoidCallback? onOpenChat;
const PartyToasts({super.key, this.life = const Duration(seconds: 5), this.onOpenChat});
@override
State<PartyToasts> createState() => _PartyToastsState();
@@ -101,13 +104,37 @@ class _PartyToastsState extends State<PartyToasts> {
super.dispose();
}
void _open(ShellToast t) {
widget.onOpenChat?.call();
setState(() => t.leaving = true);
}
@override
Widget build(BuildContext context) => IgnorePointer(child: ToastColumn(toasts: _shown.reversed.take(6).toList().reversed.toList()));
Widget build(BuildContext context) => ToastColumn(
toasts: _shown.reversed.take(6).toList().reversed.toList(),
onTap: widget.onOpenChat == null ? null : _open,
);
}
class ToastColumn extends StatelessWidget {
final List<ShellToast> toasts;
const ToastColumn({super.key, required this.toasts});
/// Clicks on chat toasts; null (or any other toast): clicks pass through.
final ValueChanged<ShellToast>? onTap;
const ToastColumn({super.key, required this.toasts, this.onTap});
Widget _card(ShellToast t) {
final card = ToastCard(toast: t);
final tap = onTap;
if (!t.chat || tap == null || t.leaving) return IgnorePointer(child: card);
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
key: ValueKey('toast-open-chat-${t.id}'),
onTap: () => tap(t),
child: card,
),
);
}
@override
Widget build(BuildContext context) {
@@ -139,7 +166,7 @@ class ToastColumn extends StatelessWidget {
duration: easeTime,
curve: ease,
opacity: t.leaving ? 0 : 1,
child: Padding(padding: const EdgeInsets.only(top: 8), child: ToastCard(toast: t)),
child: Padding(padding: const EdgeInsets.only(top: 8), child: _card(t)),
),
),
),
+5 -3
View File
@@ -4,13 +4,15 @@ import 'package:flutter/services.dart';
import '../state/session_state.dart';
import 'text_focus.dart';
/// Tab and Shift+Tab never move focus (the app's [Shortcuts] override):
/// a button that got focus that way would be pressed by Space, the ready
/// key.
/// Tab and Shift+Tab never move focus, and Space never presses the
/// focused button (the app's [Shortcuts] override): Space is the ready key,
/// and a button keeps focus after a click. Text fields still type spaces
/// (their own shortcuts come first).
Map<ShortcutActivator, Intent> get appShortcuts => {
...WidgetsApp.defaultShortcuts,
const SingleActivator(LogicalKeyboardKey.tab): const DoNothingIntent(),
const SingleActivator(LogicalKeyboardKey.tab, shift: true): const DoNothingIntent(),
const SingleActivator(LogicalKeyboardKey.space): const DoNothingIntent(),
};
/// The window's two app-wide keys, ahead of whatever has focus: while in a