Skip to content

Commit

Permalink
Added Mac OS selector handlers for insert tab and cancel op, on Mac s…
Browse files Browse the repository at this point in the history
…end all key events directly to OS, add SuperTextField demo app entyrpoint (Resolves #1583) (#1596)
  • Loading branch information
matthew-carroll authored and web-flow committed Nov 14, 2023
1 parent 621113d commit 8aa3231
Show file tree
Hide file tree
Showing 9 changed files with 403 additions and 41 deletions.
6 changes: 6 additions & 0 deletions super_editor/.run/Super Text Field (debug).run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Super Text Field (debug)" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/example/lib/main_super_text_field.dart" />
<method v="2" />
</configuration>
</component>
156 changes: 156 additions & 0 deletions super_editor/example/lib/main_super_text_field.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:super_editor/super_editor.dart';
import 'package:super_editor/super_text_field.dart';

/// An app that demos [SuperTextField].
void main() {
runApp(
MaterialApp(
home: _SuperTextFieldDemo(),
),
);
}

class _SuperTextFieldDemo extends StatefulWidget {
const _SuperTextFieldDemo();

@override
State<_SuperTextFieldDemo> createState() => _SuperTextFieldDemoState();
}

class _SuperTextFieldDemoState extends State<_SuperTextFieldDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 500),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_SingleLineTextField(),
const SizedBox(height: 16),
_MultiLineTextField(),
],
),
),
),
);
}
}

class _SingleLineTextField extends StatefulWidget {
const _SingleLineTextField();

@override
State<_SingleLineTextField> createState() => _SingleLineTextFieldState();
}

class _SingleLineTextFieldState extends State<_SingleLineTextField> {
final _focusNode = FocusNode();
final _textController = ImeAttributedTextEditingController(
controller: AttributedTextEditingController(),
);

@override
void dispose() {
_textController.dispose();
_focusNode.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return TapRegion(
groupId: "textfields",
onTapOutside: (_) => _focusNode.unfocus(),
child: TextFieldBorder(
focusNode: _focusNode,
borderBuilder: _borderBuilder,
child: SuperTextField(
focusNode: _focusNode,
textController: _textController,
textStyleBuilder: _textStyleBuilder,
hintBuilder: _createHintBuilder("Enter single line text..."),
padding: const EdgeInsets.all(4),
minLines: 1,
maxLines: 1,
inputSource: TextInputSource.ime,
),
),
);
}
}

class _MultiLineTextField extends StatefulWidget {
const _MultiLineTextField();

@override
State<_MultiLineTextField> createState() => _MultiLineTextFieldState();
}

class _MultiLineTextFieldState extends State<_MultiLineTextField> {
final _focusNode = FocusNode();
final _textController = ImeAttributedTextEditingController(
controller: AttributedTextEditingController(),
);

@override
void dispose() {
_textController.dispose();
_focusNode.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return TapRegion(
groupId: "textfields",
onTapOutside: (_) => _focusNode.unfocus(),
child: TextFieldBorder(
focusNode: _focusNode,
borderBuilder: _borderBuilder,
child: SuperTextField(
focusNode: _focusNode,
textController: _textController,
textStyleBuilder: _textStyleBuilder,
hintBuilder: _createHintBuilder("Type some text..."),
padding: const EdgeInsets.all(4),
minLines: 5,
maxLines: 5,
inputSource: TextInputSource.ime,
),
),
);
}
}

BoxDecoration _borderBuilder(TextFieldBorderState borderState) {
return BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: borderState.hasError //
? Colors.red
: borderState.hasFocus
? Colors.blue
: Colors.grey.shade300,
width: borderState.hasError ? 2 : 1,
),
);
}

TextStyle _textStyleBuilder(Set<Attribution> attributions) {
return defaultTextFieldStyleBuilder(attributions).copyWith(
color: Colors.black,
);
}

WidgetBuilder _createHintBuilder(String hintText) {
return (BuildContext context) {
return Text(
hintText,
style: TextStyle(color: Colors.grey),
);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ class SuperDesktopTextFieldState extends State<SuperDesktopTextField> implements
void _createTextFieldContext() {
_textFieldContext = SuperTextFieldContext(
textFieldBuildContext: context,
focusNode: _focusNode,
controller: _controller,
getTextLayout: () => textLayout,
scroller: _textFieldScroller,
Expand Down Expand Up @@ -989,7 +990,15 @@ class _SuperTextFieldKeyboardInteractorState extends State<SuperTextFieldKeyboar
}

_log.finest("Key handler result: $result");
return result == TextFieldKeyboardHandlerResult.handled ? KeyEventResult.handled : KeyEventResult.ignored;
switch (result) {
case TextFieldKeyboardHandlerResult.handled:
return KeyEventResult.handled;
case TextFieldKeyboardHandlerResult.sendToOperatingSystem:
return KeyEventResult.skipRemainingHandlers;
case TextFieldKeyboardHandlerResult.blocked:
case TextFieldKeyboardHandlerResult.notHandled:
return KeyEventResult.ignored;
}
}

@override
Expand Down Expand Up @@ -1675,6 +1684,20 @@ enum TextFieldKeyboardHandlerResult {
/// listeners.
blocked,

/// The handler recognized the key event but chose to
/// take no action.
///
/// No other handler should receive the key event.
///
/// The key event shouldn't bubble up the Flutter tree,
/// but it should be sent to the operating system (rather
/// than being consumed and disposed).
///
/// Use this result, for example, when Mac OS needs to
/// convert a key event into a selector, and send that
/// selector through the IME.
sendToOperatingSystem,

/// The handler has no relation to the key event and
/// took no action.
///
Expand Down Expand Up @@ -2196,7 +2219,7 @@ class DefaultSuperTextFieldKeyboardHandlers {
// For the full list of selectors handled by SuperEditor, see the MacOsSelectors class.
//
// This is needed for the interaction with the accent panel to work.
return TextFieldKeyboardHandlerResult.blocked;
return TextFieldKeyboardHandlerResult.sendToOperatingSystem;
}

return TextFieldKeyboardHandlerResult.notHandled;
Expand Down Expand Up @@ -2251,6 +2274,10 @@ typedef SuperTextFieldSelectorHandler = void Function({
});

const defaultTextFieldSelectorHandlers = <String, SuperTextFieldSelectorHandler>{
// Control.
MacOsSelectors.insertTab: _moveFocusNext,
MacOsSelectors.cancelOperation: _giveUpFocus,

// Caret movement.
MacOsSelectors.moveLeft: _moveCaretUpstream,
MacOsSelectors.moveRight: _moveCaretDownstream,
Expand Down Expand Up @@ -2283,6 +2310,18 @@ const defaultTextFieldSelectorHandlers = <String, SuperTextFieldSelectorHandler>
MacOsSelectors.deleteBackwardByDecomposingPreviousCharacter: _deleteUpstream,
};

void _giveUpFocus({
required SuperTextFieldContext textFieldContext,
}) {
textFieldContext.focusNode.unfocus();
}

void _moveFocusNext({
required SuperTextFieldContext textFieldContext,
}) {
textFieldContext.focusNode.nextFocus();
}

void _moveCaretUpstream({
required SuperTextFieldContext textFieldContext,
}) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

/// A border that displays with different colors based on common text field states, e.g.,
/// non-focused, focused, error.
///
/// The border visuals are chosen by a provided [borderBuilder]. The border state is refreshed,
/// and the [borderBuilder] is re-run, every time focus changes, or the error state changes.
class TextFieldBorder extends StatelessWidget {
const TextFieldBorder({
super.key,
required this.focusNode,
this.hasError,
required this.borderBuilder,
this.clipBehavior = Clip.none,
required this.child,
});

/// The [FocusNode] associated with the [child] text field.
final FocusNode focusNode;

/// Whether the [child] text field is currently in an error state.
final ValueListenable<bool>? hasError;

/// Creates a visual border decoration based on a given [TextFieldBorderState].
final TextFieldBorderBuilder borderBuilder;

/// Clipping strategy, which defaults to [Clip.none], and can be used to clip [child]
/// text field content when rounded corners are used for the border.
final Clip clipBehavior;

/// The widget subtree that displays a text field, to which this border applies.
final Widget child;

@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: hasError ?? ValueNotifier<bool>(false),
builder: (context, hasError, child) {
return ListenableBuilder(
listenable: focusNode,
builder: (context, child) {
return Container(
decoration: borderBuilder(_borderState),
clipBehavior: clipBehavior,
child: child,
);
},
child: child,
);
},
child: child,
);
}

TextFieldBorderState get _borderState => TextFieldBorderState(
hasFocus: focusNode.hasFocus,
hasPrimaryFocus: focusNode.hasPrimaryFocus,
hasError: hasError?.value ?? false,
);
}

/// Properties that might impact the visual appearance of a text field border.
///
/// [TextFieldBorder] provides a [TextFieldBorderState] to a [TextFieldBorderBuilder]
/// to create the desired visual border for a text field.
class TextFieldBorderState {
const TextFieldBorderState({
required this.hasFocus,
required this.hasPrimaryFocus,
required this.hasError,
});

final bool hasFocus;
final bool hasPrimaryFocus;
final bool hasError;
}

typedef TextFieldBorderBuilder = BoxDecoration Function(TextFieldBorderState borderState);
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:super_text_layout/super_text_layout.dart';
class SuperTextFieldContext {
SuperTextFieldContext({
required this.textFieldBuildContext,
required this.focusNode,
required this.controller,
required this.getTextLayout,
required this.scroller,
Expand All @@ -23,6 +24,9 @@ class SuperTextFieldContext {
/// associated render object will match the outer bounds of the text field.
final BuildContext textFieldBuildContext;

/// The [FocusNode] associated with the text field.
final FocusNode focusNode;

/// Controller that owns the text content, selection, composing region and any
/// other text-editing state for the associated text field.
final AttributedTextEditingController controller;
Expand Down
1 change: 1 addition & 0 deletions super_editor/lib/super_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export 'src/infrastructure/platforms/ios/ios_document_controls.dart';
export 'src/infrastructure/platforms/ios/floating_cursor.dart';
export 'src/infrastructure/platforms/ios/toolbar.dart';
export 'src/infrastructure/platforms/ios/magnifier.dart';
export 'src/infrastructure/platforms/mac/mac_ime.dart';
export 'src/infrastructure/platforms/mobile_documents.dart';
export 'src/infrastructure/scrolling_diagnostics/scrolling_diagnostics.dart';
export 'src/infrastructure/signal_notifier.dart';
Expand Down
4 changes: 4 additions & 0 deletions super_editor/lib/super_text_field.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
library super_text_field;

// The whole text field.
export 'src/super_textfield/super_textfield.dart';

// Tools for building new text fields.
export 'src/super_textfield/infrastructure/text_field_border.dart';
Loading

0 comments on commit 8aa3231

Please sign in to comment.