improve gesture ui and matching
This commit is contained in:
@@ -34,7 +34,7 @@ final class AccountAuthRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$accountAuthRepositoryHash() =>
|
String _$accountAuthRepositoryHash() =>
|
||||||
r'604bd3954347d0ed6cd4f894bff8388642787104';
|
r'e33a4577f8da11567f1412e615a3e020535c8f46';
|
||||||
|
|
||||||
abstract class _$AccountAuthRepository
|
abstract class _$AccountAuthRepository
|
||||||
extends $AsyncNotifier<AccountAuthState> {
|
extends $AsyncNotifier<AccountAuthState> {
|
||||||
|
|||||||
+7
@@ -667,6 +667,10 @@ class _QuickTogglesGrid extends ConsumerWidget {
|
|||||||
(s) => s.copyWith(active: !gestureSettings.active),
|
(s) => s.copyWith(active: !gestureSettings.active),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
unawaited(GestureSettingsRoute().push(context));
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -685,6 +689,7 @@ class _QuickToggle {
|
|||||||
final bool active;
|
final bool active;
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
final VoidCallback? onLongPress;
|
||||||
|
|
||||||
const _QuickToggle({
|
const _QuickToggle({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
@@ -692,6 +697,7 @@ class _QuickToggle {
|
|||||||
required this.active,
|
required this.active,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
|
this.onLongPress,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,6 +778,7 @@ class _QuickToggleSegment extends StatelessWidget {
|
|||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: toggle.enabled ? toggle.onTap : null,
|
onTap: toggle.enabled ? toggle.onTap : null,
|
||||||
|
onLongPress: toggle.enabled ? toggle.onLongPress : null,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ const defaultGestureIntervalMs = 0;
|
|||||||
const minGestureIntervalMs = 0;
|
const minGestureIntervalMs = 0;
|
||||||
const maxGestureIntervalMs = 2000;
|
const maxGestureIntervalMs = 2000;
|
||||||
|
|
||||||
|
const defaultGestureStrokeIntervalMs = 0;
|
||||||
|
const minGestureStrokeIntervalMs = 0;
|
||||||
|
const maxGestureStrokeIntervalMs = 500;
|
||||||
|
|
||||||
/// Minimum number of strokes drawn before the live overlay starts suggesting
|
/// Minimum number of strokes drawn before the live overlay starts suggesting
|
||||||
/// the other possible completions (mirrors the reference add-on's
|
/// the other possible completions (mirrors the reference add-on's
|
||||||
/// `toastMinStroke`).
|
/// `toastMinStroke`).
|
||||||
@@ -82,6 +86,11 @@ class GestureSettings with FastEquatable {
|
|||||||
/// gestures are ignored. 0 disables the cooldown.
|
/// gestures are ignored. 0 disables the cooldown.
|
||||||
final int intervalMs;
|
final int intervalMs;
|
||||||
|
|
||||||
|
/// Minimum milliseconds required between two consecutive direction changes
|
||||||
|
/// within a single gesture. A faster direction change aborts the in-progress
|
||||||
|
/// gesture, guarding against accidental fast scribbles. 0 disables the check.
|
||||||
|
final int minStrokeIntervalMs;
|
||||||
|
|
||||||
/// Whether to show the live feedback overlay while a stroke is being drawn
|
/// Whether to show the live feedback overlay while a stroke is being drawn
|
||||||
/// (the in-progress arrows plus the matching/possible actions).
|
/// (the in-progress arrows plus the matching/possible actions).
|
||||||
final bool showFeedback;
|
final bool showFeedback;
|
||||||
@@ -108,6 +117,7 @@ class GestureSettings with FastEquatable {
|
|||||||
required this.timeoutMs,
|
required this.timeoutMs,
|
||||||
required this.maxFingers,
|
required this.maxFingers,
|
||||||
required this.intervalMs,
|
required this.intervalMs,
|
||||||
|
required this.minStrokeIntervalMs,
|
||||||
required this.showFeedback,
|
required this.showFeedback,
|
||||||
required this.suggestNext,
|
required this.suggestNext,
|
||||||
required this.minSuggestionStroke,
|
required this.minSuggestionStroke,
|
||||||
@@ -122,6 +132,7 @@ class GestureSettings with FastEquatable {
|
|||||||
int? timeoutMs,
|
int? timeoutMs,
|
||||||
int? maxFingers,
|
int? maxFingers,
|
||||||
int? intervalMs,
|
int? intervalMs,
|
||||||
|
int? minStrokeIntervalMs,
|
||||||
bool? showFeedback,
|
bool? showFeedback,
|
||||||
bool? suggestNext,
|
bool? suggestNext,
|
||||||
int? minSuggestionStroke,
|
int? minSuggestionStroke,
|
||||||
@@ -133,6 +144,8 @@ class GestureSettings with FastEquatable {
|
|||||||
timeoutMs = timeoutMs ?? defaultGestureTimeoutMs,
|
timeoutMs = timeoutMs ?? defaultGestureTimeoutMs,
|
||||||
maxFingers = maxFingers ?? defaultGestureMaxFingers,
|
maxFingers = maxFingers ?? defaultGestureMaxFingers,
|
||||||
intervalMs = intervalMs ?? defaultGestureIntervalMs,
|
intervalMs = intervalMs ?? defaultGestureIntervalMs,
|
||||||
|
minStrokeIntervalMs =
|
||||||
|
minStrokeIntervalMs ?? defaultGestureStrokeIntervalMs,
|
||||||
showFeedback = showFeedback ?? true,
|
showFeedback = showFeedback ?? true,
|
||||||
suggestNext = suggestNext ?? true,
|
suggestNext = suggestNext ?? true,
|
||||||
minSuggestionStroke =
|
minSuggestionStroke =
|
||||||
@@ -156,6 +169,7 @@ class GestureSettings with FastEquatable {
|
|||||||
timeoutMs,
|
timeoutMs,
|
||||||
maxFingers,
|
maxFingers,
|
||||||
intervalMs,
|
intervalMs,
|
||||||
|
minStrokeIntervalMs,
|
||||||
showFeedback,
|
showFeedback,
|
||||||
suggestNext,
|
suggestNext,
|
||||||
minSuggestionStroke,
|
minSuggestionStroke,
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ abstract class _$GestureSettingsCWProxy {
|
|||||||
|
|
||||||
GestureSettings intervalMs(int intervalMs);
|
GestureSettings intervalMs(int intervalMs);
|
||||||
|
|
||||||
|
GestureSettings minStrokeIntervalMs(int minStrokeIntervalMs);
|
||||||
|
|
||||||
GestureSettings showFeedback(bool showFeedback);
|
GestureSettings showFeedback(bool showFeedback);
|
||||||
|
|
||||||
GestureSettings suggestNext(bool suggestNext);
|
GestureSettings suggestNext(bool suggestNext);
|
||||||
@@ -43,6 +45,7 @@ abstract class _$GestureSettingsCWProxy {
|
|||||||
int timeoutMs,
|
int timeoutMs,
|
||||||
int maxFingers,
|
int maxFingers,
|
||||||
int intervalMs,
|
int intervalMs,
|
||||||
|
int minStrokeIntervalMs,
|
||||||
bool showFeedback,
|
bool showFeedback,
|
||||||
bool suggestNext,
|
bool suggestNext,
|
||||||
int minSuggestionStroke,
|
int minSuggestionStroke,
|
||||||
@@ -76,6 +79,10 @@ class _$GestureSettingsCWProxyImpl implements _$GestureSettingsCWProxy {
|
|||||||
@override
|
@override
|
||||||
GestureSettings intervalMs(int intervalMs) => call(intervalMs: intervalMs);
|
GestureSettings intervalMs(int intervalMs) => call(intervalMs: intervalMs);
|
||||||
|
|
||||||
|
@override
|
||||||
|
GestureSettings minStrokeIntervalMs(int minStrokeIntervalMs) =>
|
||||||
|
call(minStrokeIntervalMs: minStrokeIntervalMs);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
GestureSettings showFeedback(bool showFeedback) =>
|
GestureSettings showFeedback(bool showFeedback) =>
|
||||||
call(showFeedback: showFeedback);
|
call(showFeedback: showFeedback);
|
||||||
@@ -111,6 +118,7 @@ class _$GestureSettingsCWProxyImpl implements _$GestureSettingsCWProxy {
|
|||||||
Object? timeoutMs = const $CopyWithPlaceholder(),
|
Object? timeoutMs = const $CopyWithPlaceholder(),
|
||||||
Object? maxFingers = const $CopyWithPlaceholder(),
|
Object? maxFingers = const $CopyWithPlaceholder(),
|
||||||
Object? intervalMs = const $CopyWithPlaceholder(),
|
Object? intervalMs = const $CopyWithPlaceholder(),
|
||||||
|
Object? minStrokeIntervalMs = const $CopyWithPlaceholder(),
|
||||||
Object? showFeedback = const $CopyWithPlaceholder(),
|
Object? showFeedback = const $CopyWithPlaceholder(),
|
||||||
Object? suggestNext = const $CopyWithPlaceholder(),
|
Object? suggestNext = const $CopyWithPlaceholder(),
|
||||||
Object? minSuggestionStroke = const $CopyWithPlaceholder(),
|
Object? minSuggestionStroke = const $CopyWithPlaceholder(),
|
||||||
@@ -145,6 +153,12 @@ class _$GestureSettingsCWProxyImpl implements _$GestureSettingsCWProxy {
|
|||||||
? _value.intervalMs
|
? _value.intervalMs
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: intervalMs as int,
|
: intervalMs as int,
|
||||||
|
minStrokeIntervalMs:
|
||||||
|
minStrokeIntervalMs == const $CopyWithPlaceholder() ||
|
||||||
|
minStrokeIntervalMs == null
|
||||||
|
? _value.minStrokeIntervalMs
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: minStrokeIntervalMs as int,
|
||||||
showFeedback:
|
showFeedback:
|
||||||
showFeedback == const $CopyWithPlaceholder() || showFeedback == null
|
showFeedback == const $CopyWithPlaceholder() || showFeedback == null
|
||||||
? _value.showFeedback
|
? _value.showFeedback
|
||||||
@@ -193,6 +207,7 @@ GestureSettings _$GestureSettingsFromJson(Map<String, dynamic> json) =>
|
|||||||
timeoutMs: (json['timeoutMs'] as num?)?.toInt(),
|
timeoutMs: (json['timeoutMs'] as num?)?.toInt(),
|
||||||
maxFingers: (json['maxFingers'] as num?)?.toInt(),
|
maxFingers: (json['maxFingers'] as num?)?.toInt(),
|
||||||
intervalMs: (json['intervalMs'] as num?)?.toInt(),
|
intervalMs: (json['intervalMs'] as num?)?.toInt(),
|
||||||
|
minStrokeIntervalMs: (json['minStrokeIntervalMs'] as num?)?.toInt(),
|
||||||
showFeedback: json['showFeedback'] as bool?,
|
showFeedback: json['showFeedback'] as bool?,
|
||||||
suggestNext: json['suggestNext'] as bool?,
|
suggestNext: json['suggestNext'] as bool?,
|
||||||
minSuggestionStroke: (json['minSuggestionStroke'] as num?)?.toInt(),
|
minSuggestionStroke: (json['minSuggestionStroke'] as num?)?.toInt(),
|
||||||
@@ -212,6 +227,7 @@ Map<String, dynamic> _$GestureSettingsToJson(GestureSettings instance) =>
|
|||||||
'timeoutMs': instance.timeoutMs,
|
'timeoutMs': instance.timeoutMs,
|
||||||
'maxFingers': instance.maxFingers,
|
'maxFingers': instance.maxFingers,
|
||||||
'intervalMs': instance.intervalMs,
|
'intervalMs': instance.intervalMs,
|
||||||
|
'minStrokeIntervalMs': instance.minStrokeIntervalMs,
|
||||||
'showFeedback': instance.showFeedback,
|
'showFeedback': instance.showFeedback,
|
||||||
'suggestNext': instance.suggestNext,
|
'suggestNext': instance.suggestNext,
|
||||||
'minSuggestionStroke': instance.minSuggestionStroke,
|
'minSuggestionStroke': instance.minSuggestionStroke,
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ class GestureSettingsRepository extends _$GestureSettingsRepository {
|
|||||||
DriftSqlType.int,
|
DriftSqlType.int,
|
||||||
db.typeMapping,
|
db.typeMapping,
|
||||||
),
|
),
|
||||||
|
'minStrokeIntervalMs': settings['minStrokeIntervalMs']?.readAs(
|
||||||
|
DriftSqlType.int,
|
||||||
|
db.typeMapping,
|
||||||
|
),
|
||||||
'showFeedback': settings['showFeedback']?.readAs(
|
'showFeedback': settings['showFeedback']?.readAs(
|
||||||
DriftSqlType.bool,
|
DriftSqlType.bool,
|
||||||
db.typeMapping,
|
db.typeMapping,
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ GestureConfig gestureNativeConfig(Ref ref) {
|
|||||||
strokeSize: settings.strokeSize,
|
strokeSize: settings.strokeSize,
|
||||||
timeoutMs: settings.timeoutMs,
|
timeoutMs: settings.timeoutMs,
|
||||||
maxFingers: requiredFingers,
|
maxFingers: requiredFingers,
|
||||||
|
minStrokeIntervalMs: settings.minStrokeIntervalMs,
|
||||||
activeGestureKeys: settings.bindings.keys.toList(),
|
activeGestureKeys: settings.bindings.keys.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ final class GestureNativeConfigProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$gestureNativeConfigHash() =>
|
String _$gestureNativeConfigHash() =>
|
||||||
r'57aada8631dfc3d8355f9498f6b7c6954b0cdcf1';
|
r'd7caf199a60089109cbf264ca3c4d59ccf97044e';
|
||||||
|
|
||||||
/// The in-progress stroke shown by the live feedback overlay.
|
/// The in-progress stroke shown by the live feedback overlay.
|
||||||
///
|
///
|
||||||
|
|||||||
+113
-7
@@ -29,10 +29,10 @@ const List<SettingsSectionDefinition> _behaviorSections = [
|
|||||||
title: 'Strokes',
|
title: 'Strokes',
|
||||||
entries: [
|
entries: [
|
||||||
SettingsEntryDefinition(
|
SettingsEntryDefinition(
|
||||||
title: 'Stroke sensitivity',
|
title: 'Minimum stroke length',
|
||||||
subtitle: 'Minimum swipe length recognised as a direction',
|
subtitle: 'Minimum swipe length recognised as a direction',
|
||||||
keywords: ['size', 'length'],
|
keywords: ['size', 'length', 'sensitivity'],
|
||||||
child: _StrokeSensitivitySection(),
|
child: _StrokeLengthSection(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -51,6 +51,12 @@ const List<SettingsSectionDefinition> _behaviorSections = [
|
|||||||
keywords: ['interval'],
|
keywords: ['interval'],
|
||||||
child: _CooldownSection(),
|
child: _CooldownSection(),
|
||||||
),
|
),
|
||||||
|
SettingsEntryDefinition(
|
||||||
|
title: 'Stroke interval',
|
||||||
|
subtitle: 'Reject a gesture when direction changes come too fast',
|
||||||
|
keywords: ['debounce', 'jitter', 'accidental'],
|
||||||
|
child: _StrokeIntervalSection(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -63,15 +69,68 @@ class GestureBehaviorScreen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const SettingsDetailScaffold(
|
return const SettingsDetailScaffold(
|
||||||
title: 'Behavior & timing',
|
title: 'Behavior & timing',
|
||||||
subtitle: 'Stroke sensitivity, timeout, and cooldown.',
|
subtitle: 'Stroke length, timeout, and cooldown.',
|
||||||
icon: Icons.tune,
|
icon: Icons.tune,
|
||||||
sections: _behaviorSections,
|
sections: _behaviorSections,
|
||||||
|
actions: [_ResetBehaviorButton()],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StrokeSensitivitySection extends HookConsumerWidget {
|
/// App-bar action that restores the behavior & timing fields (stroke length,
|
||||||
const _StrokeSensitivitySection();
|
/// timeout, cooldown, stroke interval) to their defaults, leaving bindings,
|
||||||
|
/// excluded sites and feedback untouched.
|
||||||
|
class _ResetBehaviorButton extends ConsumerWidget {
|
||||||
|
const _ResetBehaviorButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.settings_backup_restore),
|
||||||
|
tooltip: 'Reset to defaults',
|
||||||
|
onPressed: () async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
icon: const Icon(Icons.settings_backup_restore),
|
||||||
|
title: const Text('Reset behavior & timing?'),
|
||||||
|
content: const Text(
|
||||||
|
'Stroke length, timeout, cooldown and stroke interval will be '
|
||||||
|
'restored to their defaults. Your gesture bindings and other '
|
||||||
|
'settings are kept.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Reset'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed != true) return;
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(gestureSettingsRepositoryProvider.notifier)
|
||||||
|
.updateSettings(
|
||||||
|
(current) => current.copyWith(
|
||||||
|
strokeSize: defaultGestureStrokeSize,
|
||||||
|
timeoutMs: defaultGestureTimeoutMs,
|
||||||
|
intervalMs: defaultGestureIntervalMs,
|
||||||
|
minStrokeIntervalMs: defaultGestureStrokeIntervalMs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StrokeLengthSection extends HookConsumerWidget {
|
||||||
|
const _StrokeLengthSection();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -79,7 +138,7 @@ class _StrokeSensitivitySection extends HookConsumerWidget {
|
|||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(MdiIcons.gestureTap),
|
leading: const Icon(MdiIcons.gestureTap),
|
||||||
title: const Text('Stroke sensitivity'),
|
title: const Text('Minimum stroke length'),
|
||||||
subtitle: Slider.adaptive(
|
subtitle: Slider.adaptive(
|
||||||
min: minGestureStrokeSize.toDouble(),
|
min: minGestureStrokeSize.toDouble(),
|
||||||
max: maxGestureStrokeSize.toDouble(),
|
max: maxGestureStrokeSize.toDouble(),
|
||||||
@@ -179,3 +238,50 @@ class _CooldownSection extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StrokeIntervalSection extends HookConsumerWidget {
|
||||||
|
const _StrokeIntervalSection();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final settings = ref.watch(gestureSettingsWithDefaultsProvider);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.gesture),
|
||||||
|
title: const Text('Stroke interval'),
|
||||||
|
subtitle: Slider.adaptive(
|
||||||
|
min: minGestureStrokeIntervalMs.toDouble(),
|
||||||
|
max: maxGestureStrokeIntervalMs.toDouble(),
|
||||||
|
divisions:
|
||||||
|
(maxGestureStrokeIntervalMs - minGestureStrokeIntervalMs) ~/ 25,
|
||||||
|
value: settings.minStrokeIntervalMs
|
||||||
|
.clamp(minGestureStrokeIntervalMs, maxGestureStrokeIntervalMs)
|
||||||
|
.toDouble(),
|
||||||
|
label: settings.minStrokeIntervalMs == 0
|
||||||
|
? 'Off'
|
||||||
|
: '${settings.minStrokeIntervalMs} ms',
|
||||||
|
onChanged: (value) async {
|
||||||
|
await ref
|
||||||
|
.read(gestureSettingsRepositoryProvider.notifier)
|
||||||
|
.updateSettings(
|
||||||
|
(current) =>
|
||||||
|
current.copyWith.minStrokeIntervalMs(value.round()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(72, 0, 16, 8),
|
||||||
|
child: Text(
|
||||||
|
'Minimum time between direction changes within one gesture. '
|
||||||
|
'Faster changes abort the gesture, guarding against accidental '
|
||||||
|
'triggers.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -121,7 +121,7 @@ class GestureSettingsScreen extends HookConsumerWidget {
|
|||||||
leading: const Icon(Icons.tune),
|
leading: const Icon(Icons.tune),
|
||||||
title: const Text('Behavior & timing'),
|
title: const Text('Behavior & timing'),
|
||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
'Sensitivity, timeout, cooldown',
|
'Stroke length, timeout, cooldown',
|
||||||
),
|
),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () => open(const GestureBehaviorScreen()),
|
onTap: () => open(const GestureBehaviorScreen()),
|
||||||
|
|||||||
+34
-20
@@ -32,7 +32,9 @@ Future<GestureAction?> showGestureActionPicker(
|
|||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
useSafeArea: true,
|
useSafeArea: true,
|
||||||
showDragHandle: true,
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
),
|
||||||
builder: (context) => _GestureActionPicker(selected: selected),
|
builder: (context) => _GestureActionPicker(selected: selected),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -52,27 +54,39 @@ class _GestureActionPicker extends StatelessWidget {
|
|||||||
byCategory.putIfAbsent(action.category, () => []).add(action);
|
byCategory.putIfAbsent(action.category, () => []).add(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cap the sheet height so the long action list scrolls inside a sheet that
|
// A draggable sheet (matching the main browser menu) so the whole surface —
|
||||||
// never covers the whole screen.
|
// not just a small handle — can be swiped down to dismiss.
|
||||||
final maxHeight = MediaQuery.sizeOf(context).height * 0.8;
|
return DraggableScrollableSheet(
|
||||||
|
initialChildSize: 0.7,
|
||||||
return ConstrainedBox(
|
minChildSize: 0.4,
|
||||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
maxChildSize: 0.95,
|
||||||
child: SafeArea(
|
expand: false,
|
||||||
top: false,
|
builder: (context, scrollController) {
|
||||||
child: Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
// Drag handle.
|
||||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 8),
|
Container(
|
||||||
child: Text(
|
margin: const EdgeInsets.only(top: 12, bottom: 8),
|
||||||
'Choose action',
|
height: 4,
|
||||||
style: theme.textTheme.titleLarge,
|
width: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Flexible(
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 0, 24, 8),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
'Choose action',
|
||||||
|
style: theme.textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
controller: scrollController,
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
children: [
|
children: [
|
||||||
for (final category in GestureActionCategory.values)
|
for (final category in GestureActionCategory.values)
|
||||||
@@ -104,8 +118,8 @@ class _GestureActionPicker extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -243,7 +243,10 @@ class _GestureBindingEditor extends HookWidget {
|
|||||||
).colorScheme.onSurfaceVariant,
|
).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: GestureStrokeView(stroke: stroke),
|
: GestureStrokeView(
|
||||||
|
stroke: stroke,
|
||||||
|
showQualifiers: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:weblibre/features/gestures/data/models/gesture_stroke.dart';
|
import 'package:weblibre/features/gestures/data/models/gesture_stroke.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/inline_count_badge.dart';
|
||||||
|
|
||||||
extension GestureArrowIcon on GestureArrow {
|
extension GestureArrowIcon on GestureArrow {
|
||||||
IconData get icon => switch (this) {
|
IconData get icon => switch (this) {
|
||||||
@@ -30,17 +32,27 @@ extension GestureArrowIcon on GestureArrow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Compact visual rendering of a [GestureStroke]: the direction arrows in
|
/// Compact visual rendering of a [GestureStroke]: the direction arrows in
|
||||||
/// sequence, prefixed by start-position and finger-count qualifiers when set.
|
/// sequence, prefixed by start-position and finger-count qualifiers.
|
||||||
///
|
///
|
||||||
/// All glyphs share a single foreground [color] so the view blends with
|
/// All glyphs share a single foreground [color] so the view blends with
|
||||||
/// whatever surface it sits on; it defaults to the primary color, but callers
|
/// whatever surface it sits on; it defaults to the primary color, but callers
|
||||||
/// rendering on a contrasting surface (e.g. the live overlay) should pass the
|
/// rendering on a contrasting surface (e.g. the live overlay) should pass the
|
||||||
/// matching `on…` color.
|
/// matching `on…` color.
|
||||||
|
///
|
||||||
|
/// Set [showQualifiers] to false to render only the direction arrows, omitting
|
||||||
|
/// the start-position and finger qualifiers (e.g. the live pattern preview in
|
||||||
|
/// the binding editor, where those are configured separately).
|
||||||
class GestureStrokeView extends StatelessWidget {
|
class GestureStrokeView extends StatelessWidget {
|
||||||
final GestureStroke stroke;
|
final GestureStroke stroke;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
|
final bool showQualifiers;
|
||||||
|
|
||||||
const GestureStrokeView({required this.stroke, this.color, super.key});
|
const GestureStrokeView({
|
||||||
|
required this.stroke,
|
||||||
|
this.color,
|
||||||
|
this.showQualifiers = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -50,7 +62,7 @@ class GestureStrokeView extends StatelessWidget {
|
|||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (stroke.startPosition != GestureStartPosition.anywhere)
|
if (showQualifiers) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 6),
|
padding: const EdgeInsets.only(right: 6),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -59,14 +71,16 @@ class GestureStrokeView extends StatelessWidget {
|
|||||||
color: foreground,
|
color: foreground,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (stroke.fingers >= 2)
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 4),
|
padding: const EdgeInsets.only(right: 6),
|
||||||
child: Text(
|
child: InlineCountBadge(
|
||||||
'${stroke.fingers}×',
|
count: stroke.fingers,
|
||||||
style: theme.textTheme.labelLarge?.copyWith(color: foreground),
|
backgroundColor: foreground,
|
||||||
|
foregroundColor: theme.colorScheme.surface,
|
||||||
|
icon: MdiIcons.buttonPointer,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
for (final arrow in stroke.arrows)
|
for (final arrow in stroke.arrows)
|
||||||
Icon(arrow.icon, size: 18, color: foreground),
|
Icon(arrow.icon, size: 18, color: foreground),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -55,4 +55,4 @@ final class ProxyConnectionOptionsProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$proxyConnectionOptionsHash() =>
|
String _$proxyConnectionOptionsHash() =>
|
||||||
r'7832968f74acd98189110cf926208e694a20d55a';
|
r'718f8d7f7fc7eb6a56bb5975bcccb64fec1675bc';
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ final class ContainerProxyRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$containerProxyRepositoryHash() =>
|
String _$containerProxyRepositoryHash() =>
|
||||||
r'ec3ec41b45b991623518f0aceb6ee7b21abb9112';
|
r'9201bb0cdda570d38adc5fc16d1c7719f3d719a2';
|
||||||
|
|
||||||
abstract class _$ContainerProxyRepository extends $Notifier<void> {
|
abstract class _$ContainerProxyRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ final class ProxyInputConsumerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$proxyInputConsumerHash() =>
|
String _$proxyInputConsumerHash() =>
|
||||||
r'8958cb3ff089779c1991390a7d6062cef74b0588';
|
r'652a9df1efd937039f62498d56e5d30f09066d91';
|
||||||
|
|
||||||
abstract class _$ProxyInputConsumer extends $Notifier<void> {
|
abstract class _$ProxyInputConsumer extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ final class ProxyLatencyResultsProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$proxyLatencyResultsHash() =>
|
String _$proxyLatencyResultsHash() =>
|
||||||
r'f86dba5c5f17d1f74fdf2b7ca6f5adaa4c42ff75';
|
r'85dec80ca28c86cf0cb62c6548d82986de79f61d';
|
||||||
|
|
||||||
/// Per-profile latency results, keyed by profile id. Holds the latest result
|
/// Per-profile latency results, keyed by profile id. Holds the latest result
|
||||||
/// only; we don't keep history because the test is user-triggered and the user
|
/// only; we don't keep history because the test is user-triggered and the user
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ final class MetaSearchControllerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$metaSearchControllerHash() =>
|
String _$metaSearchControllerHash() =>
|
||||||
r'74ea7c616213dae53d3ac18b2677b797f5f87d53';
|
r'eea46b9be86f50fb1bfcbe89fd77a68c8f2233e0';
|
||||||
|
|
||||||
abstract class _$MetaSearchController extends $Notifier<MetaSearchState> {
|
abstract class _$MetaSearchController extends $Notifier<MetaSearchState> {
|
||||||
MetaSearchState build();
|
MetaSearchState build();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class InlineCountBadge extends StatelessWidget {
|
|||||||
required this.count,
|
required this.count,
|
||||||
required this.backgroundColor,
|
required this.backgroundColor,
|
||||||
required this.foregroundColor,
|
required this.foregroundColor,
|
||||||
|
this.icon,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,6 +32,9 @@ class InlineCountBadge extends StatelessWidget {
|
|||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
final Color foregroundColor;
|
final Color foregroundColor;
|
||||||
|
|
||||||
|
/// Optional glyph rendered inside the badge, trailing the count.
|
||||||
|
final IconData? icon;
|
||||||
|
|
||||||
static String formatCount(int count) {
|
static String formatCount(int count) {
|
||||||
if (count >= 1000) {
|
if (count >= 1000) {
|
||||||
final k = count / 1000;
|
final k = count / 1000;
|
||||||
@@ -49,13 +53,22 @@ class InlineCountBadge extends StatelessWidget {
|
|||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Row(
|
||||||
formatCount(count),
|
mainAxisSize: MainAxisSize.min,
|
||||||
style: TextStyle(
|
children: [
|
||||||
fontSize: 11,
|
Text(
|
||||||
fontWeight: FontWeight.bold,
|
formatCount(count),
|
||||||
color: foregroundColor,
|
style: TextStyle(
|
||||||
),
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: foregroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (icon case final icon?) ...[
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Icon(icon, size: 12, color: foregroundColor),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -58,6 +58,7 @@ class GestureRecognizer {
|
|||||||
private var lastX = 0f
|
private var lastX = 0f
|
||||||
private var lastY = 0f
|
private var lastY = 0f
|
||||||
private var lastArrow = ' '
|
private var lastArrow = ' '
|
||||||
|
private var lastArrowTime = 0L
|
||||||
private var strokeSize = 0f
|
private var strokeSize = 0f
|
||||||
private var edgeWidth = 0f
|
private var edgeWidth = 0f
|
||||||
private var contentBottom = 0f
|
private var contentBottom = 0f
|
||||||
@@ -159,7 +160,17 @@ class GestureRecognizer {
|
|||||||
}
|
}
|
||||||
// Collapse consecutive identical directions into a single stroke.
|
// Collapse consecutive identical directions into a single stroke.
|
||||||
if (arrow == lastArrow) return
|
if (arrow == lastArrow) return
|
||||||
|
// Reject gestures whose direction changes come faster than the
|
||||||
|
// configured minimum (e.g. an accidental fast scribble). The first
|
||||||
|
// arrow has no predecessor, so it is never gated.
|
||||||
|
val minInterval = cfg.minStrokeIntervalMs
|
||||||
|
if (lastArrow != ' ' && minInterval > 0 &&
|
||||||
|
event.eventTime - lastArrowTime < minInterval) {
|
||||||
|
aborted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
lastArrow = arrow
|
lastArrow = arrow
|
||||||
|
lastArrowTime = event.eventTime
|
||||||
arrows.add(arrow)
|
arrows.add(arrow)
|
||||||
onProgress?.invoke(currentKey())
|
onProgress?.invoke(currentKey())
|
||||||
}
|
}
|
||||||
@@ -217,6 +228,7 @@ class GestureRecognizer {
|
|||||||
fingers = ""
|
fingers = ""
|
||||||
fingersNum = 1
|
fingersNum = 1
|
||||||
lastArrow = ' '
|
lastArrow = ' '
|
||||||
|
lastArrowTime = 0L
|
||||||
aborted = false
|
aborted = false
|
||||||
active = false
|
active = false
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -5599,6 +5599,13 @@ data class GestureConfig (
|
|||||||
val timeoutMs: Long,
|
val timeoutMs: Long,
|
||||||
/** Maximum number of simultaneous pointers a gesture may use. */
|
/** Maximum number of simultaneous pointers a gesture may use. */
|
||||||
val maxFingers: Long,
|
val maxFingers: Long,
|
||||||
|
/**
|
||||||
|
* Minimum milliseconds required between two consecutive direction changes
|
||||||
|
* within a single gesture. A new direction registered faster than this
|
||||||
|
* aborts the in-progress gesture, so accidental fast scribbles match
|
||||||
|
* nothing. `0` disables the check.
|
||||||
|
*/
|
||||||
|
val minStrokeIntervalMs: Long,
|
||||||
/**
|
/**
|
||||||
* Canonical keys that currently have an action bound, e.g. `D-R`,
|
* Canonical keys that currently have an action bound, e.g. `D-R`,
|
||||||
* `R:2:D-L`. Native only emits [GeckoGestureEvents.onGestureRecognized]
|
* `R:2:D-L`. Native only emits [GeckoGestureEvents.onGestureRecognized]
|
||||||
@@ -5613,8 +5620,9 @@ data class GestureConfig (
|
|||||||
val strokeSize = pigeonVar_list[1] as Long
|
val strokeSize = pigeonVar_list[1] as Long
|
||||||
val timeoutMs = pigeonVar_list[2] as Long
|
val timeoutMs = pigeonVar_list[2] as Long
|
||||||
val maxFingers = pigeonVar_list[3] as Long
|
val maxFingers = pigeonVar_list[3] as Long
|
||||||
val activeGestureKeys = pigeonVar_list[4] as List<String>
|
val minStrokeIntervalMs = pigeonVar_list[4] as Long
|
||||||
return GestureConfig(enabled, strokeSize, timeoutMs, maxFingers, activeGestureKeys)
|
val activeGestureKeys = pigeonVar_list[5] as List<String>
|
||||||
|
return GestureConfig(enabled, strokeSize, timeoutMs, maxFingers, minStrokeIntervalMs, activeGestureKeys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
@@ -5623,6 +5631,7 @@ data class GestureConfig (
|
|||||||
strokeSize,
|
strokeSize,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
maxFingers,
|
maxFingers,
|
||||||
|
minStrokeIntervalMs,
|
||||||
activeGestureKeys,
|
activeGestureKeys,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -5634,7 +5643,7 @@ data class GestureConfig (
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
val other = other as GestureConfig
|
val other = other as GestureConfig
|
||||||
return GeckoPigeonUtils.deepEquals(this.enabled, other.enabled) && GeckoPigeonUtils.deepEquals(this.strokeSize, other.strokeSize) && GeckoPigeonUtils.deepEquals(this.timeoutMs, other.timeoutMs) && GeckoPigeonUtils.deepEquals(this.maxFingers, other.maxFingers) && GeckoPigeonUtils.deepEquals(this.activeGestureKeys, other.activeGestureKeys)
|
return GeckoPigeonUtils.deepEquals(this.enabled, other.enabled) && GeckoPigeonUtils.deepEquals(this.strokeSize, other.strokeSize) && GeckoPigeonUtils.deepEquals(this.timeoutMs, other.timeoutMs) && GeckoPigeonUtils.deepEquals(this.maxFingers, other.maxFingers) && GeckoPigeonUtils.deepEquals(this.minStrokeIntervalMs, other.minStrokeIntervalMs) && GeckoPigeonUtils.deepEquals(this.activeGestureKeys, other.activeGestureKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -5643,6 +5652,7 @@ data class GestureConfig (
|
|||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.strokeSize)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.strokeSize)
|
||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.timeoutMs)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.timeoutMs)
|
||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.maxFingers)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.maxFingers)
|
||||||
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.minStrokeIntervalMs)
|
||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.activeGestureKeys)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.activeGestureKeys)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3031,6 +3031,12 @@ class GestureConfig {
|
|||||||
/// Maximum number of simultaneous pointers a gesture may use.
|
/// Maximum number of simultaneous pointers a gesture may use.
|
||||||
final int maxFingers;
|
final int maxFingers;
|
||||||
|
|
||||||
|
/// Minimum milliseconds required between two consecutive direction changes
|
||||||
|
/// within a single gesture. A new direction registered faster than this
|
||||||
|
/// aborts the in-progress gesture, so accidental fast scribbles match
|
||||||
|
/// nothing. `0` disables the check.
|
||||||
|
final int minStrokeIntervalMs;
|
||||||
|
|
||||||
/// Canonical keys that currently have an action bound, e.g. `D-R`,
|
/// Canonical keys that currently have an action bound, e.g. `D-R`,
|
||||||
/// `R:2:D-L`. Native only emits [GeckoGestureEvents.onGestureRecognized]
|
/// `R:2:D-L`. Native only emits [GeckoGestureEvents.onGestureRecognized]
|
||||||
/// when an assembled stroke matches one of these.
|
/// when an assembled stroke matches one of these.
|
||||||
@@ -3041,6 +3047,7 @@ class GestureConfig {
|
|||||||
this.strokeSize = 50,
|
this.strokeSize = 50,
|
||||||
this.timeoutMs = 1500,
|
this.timeoutMs = 1500,
|
||||||
this.maxFingers = 1,
|
this.maxFingers = 1,
|
||||||
|
this.minStrokeIntervalMs = 0,
|
||||||
this.activeGestureKeys = const [],
|
this.activeGestureKeys = const [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user