Skip to content

Commit f96668c

Browse files
committed
Improve pen input experience
1 parent 60e2b45 commit f96668c

File tree

13 files changed

+300
-44
lines changed

13 files changed

+300
-44
lines changed

app/lib/cubits/current_index.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,19 @@ sealed class CurrentIndex with _$CurrentIndex {
8888
@Default(NavigatorPage.waypoints) NavigatorPage navigatorPage,
8989
@Default(false) bool isCreating,
9090
@Default('') String userName,
91+
@Default(false) bool penDetected,
92+
@Default(false) bool sessionPenOnlyInput,
9193
}) = _CurrentIndex;
9294

95+
/// Returns the effective pen-only input state.
96+
/// If the setting is null (auto), uses the session-based state.
97+
/// Otherwise uses the persisted setting.
98+
bool get effectivePenOnlyInput {
99+
final setting = settingsCubit.state.penOnlyInput;
100+
if (setting != null) return setting;
101+
return sessionPenOnlyInput;
102+
}
103+
93104
bool get moveEnabled =>
94105
(settingsCubit.state.inputGestures && pointers.length > 1) &&
95106
settingsCubit.state.moveOnGesture;
@@ -178,6 +189,28 @@ class CurrentIndexCubit extends Cubit<CurrentIndex> {
178189
state.networkingService.setup(bloc);
179190
}
180191

192+
void setPenDetected(bool detected) {
193+
if (state.penDetected == detected) return;
194+
// When pen is detected and setting is auto (null), enable session pen-only
195+
final shouldEnableSessionPenOnly =
196+
detected &&
197+
state.settingsCubit.state.penOnlyInput == null &&
198+
!state.sessionPenOnlyInput;
199+
emit(
200+
state.copyWith(
201+
penDetected: detected,
202+
sessionPenOnlyInput: shouldEnableSessionPenOnly
203+
? true
204+
: state.sessionPenOnlyInput,
205+
),
206+
);
207+
}
208+
209+
void setSessionPenOnlyInput(bool value) {
210+
if (state.sessionPenOnlyInput == value) return;
211+
emit(state.copyWith(sessionPenOnlyInput: value));
212+
}
213+
181214
Future<void> _updateOnVisible(
182215
CameraViewport newViewport,
183216
DocumentLoaded blocState,

app/lib/cubits/current_index.freezed.dart

Lines changed: 18 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/lib/cubits/settings.dart

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,8 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings {
309309
@Default(1) double touchSensitivity,
310310
@Default(1) double selectSensitivity,
311311
@Default(1) double scrollSensitivity,
312-
@Default(false) bool penOnlyInput,
312+
bool? penOnlyInput,
313+
@Default(true) bool showPenOnlyToggle,
313314
@Default(true) bool inputGestures,
314315
@Default('') String design,
315316
@Default(BannerVisibility.always) BannerVisibility bannerVisibility,
@@ -378,7 +379,10 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings {
378379
const [];
379380
return ButterflySettings(
380381
localeTag: prefs.getString('locale') ?? '',
381-
penOnlyInput: prefs.getBool('pen_only_input') ?? false,
382+
penOnlyInput: prefs.containsKey('pen_only_input')
383+
? prefs.getBool('pen_only_input')
384+
: null,
385+
showPenOnlyToggle: prefs.getBool('show_pen_only_toggle') ?? true,
382386
inputGestures: prefs.getBool('input_gestures') ?? true,
383387
documentPath: prefs.getString('document_path') ?? '',
384388
theme: prefs.containsKey('theme_mode')
@@ -538,7 +542,10 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings {
538542
await prefs.setString('theme_mode', theme.name);
539543
await prefs.setString('theme_density', density.name);
540544
await prefs.setString('locale', localeTag);
541-
await prefs.setBool('pen_only_input', penOnlyInput);
545+
if (penOnlyInput != null) {
546+
await prefs.setBool('pen_only_input', penOnlyInput!);
547+
}
548+
await prefs.setBool('show_pen_only_toggle', showPenOnlyToggle);
542549
await prefs.setBool('input_gestures', inputGestures);
543550
await prefs.setString('document_path', documentPath);
544551
await prefs.setDouble('touch_sensitivity', touchSensitivity);
@@ -736,13 +743,19 @@ class SettingsCubit extends Cubit<ButterflySettings>
736743
return save();
737744
}
738745

739-
Future<void> changePenOnlyInput(bool penOnlyInput) {
746+
Future<void> changePenOnlyInput(bool? penOnlyInput) {
740747
emit(state.copyWith(penOnlyInput: penOnlyInput));
741748
return save();
742749
}
743750

744-
Future<void> resetPenOnlyInput() {
745-
emit(state.copyWith(penOnlyInput: false));
751+
Future<void> resetPenOnlyInput() async {
752+
final prefs = await SharedPreferences.getInstance();
753+
await prefs.remove('pen_only_input');
754+
emit(state.copyWith(penOnlyInput: null));
755+
}
756+
757+
Future<void> changeShowPenOnlyToggle(bool showPenOnlyToggle) {
758+
emit(state.copyWith(showPenOnlyToggle: showPenOnlyToggle));
746759
return save();
747760
}
748761

app/lib/cubits/settings.freezed.dart

Lines changed: 20 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/lib/cubits/settings.g.dart

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/lib/handlers/laser.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,7 @@ class LaserHandler extends Handler<LaserTool> with ColoredHandler {
123123
final currentIndexCubit = context.read<CurrentIndexCubit>();
124124
final transform = context.read<TransformCubit>().state;
125125
final state = bloc.state as DocumentLoadSuccess;
126-
final settings = context.read<SettingsCubit>().state;
127-
final penOnlyInput = settings.penOnlyInput;
126+
final penOnlyInput = currentIndexCubit.state.effectivePenOnlyInput;
128127
localPosition = PointerManipulationHandler.calculatePointerPosition(
129128
currentIndexCubit.state,
130129
localPosition,

app/lib/handlers/pen.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ class PenHandler extends Handler<PenTool> with ColoredHandler {
127127
if (!bloc.isInBounds(globalPos)) return;
128128
final state = bloc.state as DocumentLoadSuccess;
129129
final settings = context.read<SettingsCubit>().state;
130-
final penOnlyInput = settings.penOnlyInput;
130+
final penOnlyInput = currentIndexCubit.state.effectivePenOnlyInput;
131131
if (lastPosition[pointer] == localPos) return;
132132
lastPosition[pointer] = localPos;
133133
if (penOnlyInput &&

app/lib/l10n/app_en.arb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,19 @@
594594
"folderSynced": "Folder synced",
595595
"syncRootDirectory": "Sync root directory",
596596
"penOnlyInput": "Pen only input",
597+
"@penOnlyInput": {
598+
"description": "Setting to enable pen only input mode"
599+
},
600+
"penOnlyInputAutoDescription": "Automatically enables pen-only mode when a stylus is detected. This setting only lasts for the current session.",
601+
"penOnlyInputOnDescription": "Always reject touch and mouse input, only accept stylus input.",
602+
"penOnlyInputOffDescription": "Accept all input types including touch, mouse, and stylus.",
603+
"automatic": "Automatic",
604+
"alwaysOn": "Always on",
605+
"alwaysOff": "Always off",
606+
"showPenOnlyToggle": "Show pen only toggle",
607+
"@showPenOnlyToggle": {
608+
"description": "Setting to show pen only toggle button when pen is detected"
609+
},
597610
"inputGestures": "Input gestures",
598611
"nativeTitleBar": "Native title bar",
599612
"syncMode": "Sync mode",

app/lib/settings/inputs/pen.dart

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ import '../../cubits/settings.dart';
1212
class PenInputSettings extends StatelessWidget {
1313
const PenInputSettings({super.key});
1414

15+
String _getPenOnlyInputName(bool? value, BuildContext context) {
16+
if (value == null) return AppLocalizations.of(context).automatic;
17+
return value
18+
? AppLocalizations.of(context).alwaysOn
19+
: AppLocalizations.of(context).alwaysOff;
20+
}
21+
1522
String _getIgnorePressureName(
1623
IgnorePressure ignorePressure,
1724
BuildContext context,
@@ -53,17 +60,88 @@ class PenInputSettings extends StatelessWidget {
5360
child: Column(
5461
crossAxisAlignment: CrossAxisAlignment.stretch,
5562
children: [
56-
SwitchListTile(
57-
value: state.penOnlyInput,
63+
ListTile(
5864
title: Text(
5965
AppLocalizations.of(context).penOnlyInput,
6066
),
61-
secondary: const PhosphorIcon(
67+
subtitle: Text(
68+
_getPenOnlyInputName(state.penOnlyInput, context),
69+
),
70+
leading: const PhosphorIcon(
6271
PhosphorIconsLight.pencilSimpleLine,
6372
),
73+
onTap: () {
74+
final cubit = context.read<SettingsCubit>();
75+
final current = cubit.state.penOnlyInput;
76+
77+
showLeapBottomSheet(
78+
context: context,
79+
titleBuilder: (context) => Text(
80+
AppLocalizations.of(context).penOnlyInput,
81+
),
82+
childrenBuilder: (context) {
83+
return [
84+
ListTile(
85+
title: Text(
86+
AppLocalizations.of(context).automatic,
87+
),
88+
subtitle: Text(
89+
AppLocalizations.of(
90+
context,
91+
).penOnlyInputAutoDescription,
92+
),
93+
selected: current == null,
94+
onTap: () {
95+
cubit.changePenOnlyInput(null);
96+
Navigator.of(context).pop();
97+
},
98+
),
99+
ListTile(
100+
title: Text(
101+
AppLocalizations.of(context).alwaysOn,
102+
),
103+
subtitle: Text(
104+
AppLocalizations.of(
105+
context,
106+
).penOnlyInputOnDescription,
107+
),
108+
selected: current == true,
109+
onTap: () {
110+
cubit.changePenOnlyInput(true);
111+
Navigator.of(context).pop();
112+
},
113+
),
114+
ListTile(
115+
title: Text(
116+
AppLocalizations.of(context).alwaysOff,
117+
),
118+
subtitle: Text(
119+
AppLocalizations.of(
120+
context,
121+
).penOnlyInputOffDescription,
122+
),
123+
selected: current == false,
124+
onTap: () {
125+
cubit.changePenOnlyInput(false);
126+
Navigator.of(context).pop();
127+
},
128+
),
129+
];
130+
},
131+
);
132+
},
133+
),
134+
SwitchListTile(
135+
value: state.showPenOnlyToggle,
136+
title: Text(
137+
AppLocalizations.of(context).showPenOnlyToggle,
138+
),
139+
secondary: const PhosphorIcon(
140+
PhosphorIconsLight.toggleRight,
141+
),
64142
onChanged: (value) => context
65143
.read<SettingsCubit>()
66-
.changePenOnlyInput(value),
144+
.changeShowPenOnlyToggle(value),
67145
),
68146
ListTile(
69147
title: Text(

app/lib/views/main.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import '../actions/color_palette.dart';
3434
import '../models/viewport.dart';
3535
import '../services/asset.dart';
3636
import '../api/changes.dart';
37+
import 'pen_only_toggle.dart';
3738
import 'view.dart';
3839
import 'zoom.dart';
3940

@@ -565,7 +566,19 @@ class _MainBody extends StatelessWidget {
565566
crossAxisAlignment:
566567
CrossAxisAlignment.end,
567568
children: [
568-
ZoomView(isMobile: isMobile),
569+
Row(
570+
mainAxisAlignment:
571+
MainAxisAlignment.end,
572+
children: [
573+
const PenOnlyToggle(),
574+
const SizedBox(width: 8),
575+
Expanded(
576+
child: ZoomView(
577+
isMobile: isMobile,
578+
),
579+
),
580+
],
581+
),
569582
if (currentIndex.hideUi ==
570583
HideState.touch)
571584
FloatingActionButton.small(

0 commit comments

Comments
 (0)