Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cat-voices): plain text component for single and multiline #1517

Merged
merged 9 commits into from
Jan 15, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
class ResizableBoxParent extends StatelessWidget {
final bool resizableVertically;
final bool resizableHorizontally;
final bool maxLineSet;
LynxLynxx marked this conversation as resolved.
Show resolved Hide resolved
final Widget child;
final double minWidth;
final double minHeight;
Expand All @@ -18,6 +19,7 @@ class ResizableBoxParent extends StatelessWidget {
required this.child,
this.minWidth = 40,
this.minHeight = 40,
this.maxLineSet = false,
});

@override
Expand All @@ -34,6 +36,7 @@ class ResizableBoxParent extends StatelessWidget {
minHeight: minHeight,
resizableHorizontally: resizableHorizontally,
resizableVertically: resizableVertically,
maxLineSet: maxLineSet,
child: child,
);
},
Expand All @@ -48,6 +51,7 @@ class _ResizableBox extends StatefulWidget {
final double minHeight;
final bool resizableVertically;
final bool resizableHorizontally;
final bool maxLineSet;

const _ResizableBox({
required this.constraints,
Expand All @@ -56,6 +60,7 @@ class _ResizableBox extends StatefulWidget {
required this.minHeight,
required this.resizableVertically,
required this.resizableHorizontally,
required this.maxLineSet,
});

@override
Expand Down Expand Up @@ -91,7 +96,9 @@ class _ResizableBoxState extends State<_ResizableBox> {
child: widget.child,
),
Positioned(
bottom: 0,
// when maxLine is set, icon is placed at the max line text
// and not at the corner of the text field
bottom: widget.maxLineSet ? 18 : 0,
right: 0,
child: MouseRegion(
cursor: SystemMouseCursors.resizeDownRight,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import 'package:catalyst_voices/common/ext/document_property_ext.dart';
import 'package:catalyst_voices/widgets/widgets.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class SimpleTextEntryWidget extends StatefulWidget {
final DocumentProperty<String> property;
final bool isEditMode;
final ValueChanged<DocumentChange> onChanged;

const SimpleTextEntryWidget({
super.key,
required this.property,
required this.isEditMode,
required this.onChanged,
});

@override
State<SimpleTextEntryWidget> createState() => _SimpleTextEntryWidgetState();
}

class _SimpleTextEntryWidgetState extends State<SimpleTextEntryWidget> {
late final TextEditingController _controller;
late final FocusNode _focusNode;

@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.property.value);
_controller.addListener(_handleValueChange);
_focusNode = FocusNode(canRequestFocus: widget.isEditMode);
}

@override
void didUpdateWidget(SimpleTextEntryWidget oldWidget) {
super.didUpdateWidget(oldWidget);

if (oldWidget.isEditMode != widget.isEditMode) {
_handleEditModeChanged();
if (!widget.isEditMode) {
_controller.text = widget.property.value ?? '';
}
}

if (widget.property.value != oldWidget.property.value) {
_controller.text = widget.property.value ?? '';
}
}

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

String get _description => widget.property.formattedDescription;
int? get _maxLength => widget.property.schema.strLengthRange?.max;
bool get _resizable =>
widget.property.schema.definition is MultiLineTextEntryDefinition;
LynxLynxx marked this conversation as resolved.
Show resolved Hide resolved

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (_description.isNotEmpty) ...[
Text(
_description,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
],
_SimpleDocumentTextField(
controller: _controller,
focusNode: _focusNode,
onFieldSubmitted: _notifyChangeListener,
validator: _validate,
enabled: widget.isEditMode,
resizable: _resizable,
maxLength: _maxLength,
),
],
);
}

void _handleEditModeChanged() {
_focusNode.canRequestFocus = widget.isEditMode;

if (widget.isEditMode) {
_focusNode.requestFocus();
} else {
_focusNode.unfocus();
}
}

void _handleValueChange() {
final controllerValue = _controller.text;
if (widget.property.value != controllerValue &&
controllerValue.isNotEmpty) {
_notifyChangeListener(controllerValue);
}
}

void _notifyChangeListener(String? value) {
final change = DocumentChange(
nodeId: widget.property.schema.nodeId,
value: value,
);

widget.onChanged(change);
}

VoicesTextFieldValidationResult _validate(String? value) {
if (!widget.isEditMode) {
return const VoicesTextFieldValidationResult.none();
}
final schema = widget.property.schema;
final result = schema.validatePropertyValue(value);
if (result.isValid) {
return const VoicesTextFieldValidationResult.none();
} else {
final localized = LocalizedDocumentValidationResult.from(result);
return VoicesTextFieldValidationResult.error(localized.message(context));
}
}
}

class _SimpleDocumentTextField extends StatelessWidget {
final TextEditingController? controller;
final ValueChanged<String>? onFieldSubmitted;
final VoicesTextFieldValidator? validator;
final FocusNode? focusNode;
final bool enabled;
final bool resizable;
final int? maxLength;

const _SimpleDocumentTextField({
this.controller,
this.onFieldSubmitted,
this.validator,
this.focusNode,
this.enabled = false,
this.resizable = false,
this.maxLength,
});

@override
Widget build(BuildContext context) {
return VoicesTextField(
controller: controller,
focusNode: focusNode,
onFieldSubmitted: onFieldSubmitted,
validator: validator,
enabled: enabled,
resizable: resizable,
maxLengthEnforcement: MaxLengthEnforcement.none,
autovalidateMode: AutovalidateMode.disabled,
maxLines: resizable ? null : 1,
maxLength: maxLength,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ class VoicesTextField extends StatefulWidget {
/// [AutovalidateMode]
final AutovalidateMode? autovalidateMode;

/// [MaxLengthEnforcement]
final MaxLengthEnforcement? maxLengthEnforcement;

final ValueChanged<VoicesTextFieldStatus>? onStatusChanged;

const VoicesTextField({
Expand Down Expand Up @@ -113,6 +116,7 @@ class VoicesTextField extends StatefulWidget {
this.onSaved,
this.inputFormatters,
this.autovalidateMode,
this.maxLengthEnforcement,
this.onStatusChanged,
});

Expand Down Expand Up @@ -209,6 +213,8 @@ class _VoicesTextFieldState extends State<VoicesTextField> {
ResizableBoxParent(
resizableHorizontally: resizable,
resizableVertically: resizable,
minHeight: widget.maxLines == null ? 65 : 48,
maxLineSet: widget.maxLines == null,
child: TextFormField(
textAlignVertical: TextAlignVertical.top,
autofocus: widget.autofocus,
Expand All @@ -229,6 +235,7 @@ class _VoicesTextFieldState extends State<VoicesTextField> {
maxLines: widget.maxLines,
minLines: widget.minLines,
maxLength: widget.maxLength,
maxLengthEnforcement: widget.maxLengthEnforcement,
readOnly: widget.readOnly,
ignorePointers: widget.ignorePointers,
enabled: widget.enabled,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:catalyst_voices/widgets/document_builder/agreement_confirmation_widget.dart';
import 'package:catalyst_voices/widgets/document_builder/document_token_value_widget.dart';
import 'package:catalyst_voices/widgets/document_builder/simple_text_entry_widget.dart';
import 'package:catalyst_voices/widgets/document_builder/single_dropdown_selection_widget.dart';
import 'package:catalyst_voices/widgets/document_builder/single_grouped_tag_selector_widget.dart';
import 'package:catalyst_voices/widgets/document_builder/single_line_https_url_widget.dart.dart';
Expand Down Expand Up @@ -211,8 +212,7 @@ class _PropertyBuilder extends StatelessWidget {
'${property.schema.definition} unsupported '
'by $DocumentBuilderSectionTile',
);
case SingleLineTextEntryDefinition():
case MultiLineTextEntryDefinition():

case MultiLineTextEntryMarkdownDefinition():
case MultiSelectDefinition():
case SingleLineTextEntryListDefinition():
Expand Down Expand Up @@ -267,12 +267,21 @@ class _PropertyBuilder extends StatelessWidget {
onChanged: onChanged,
);
case TokenValueCardanoADADefinition():
final castProperty = definition.castProperty(property);
return DocumentTokenValueWidget(
property: definition.castProperty(property),
property: castProperty,
currency: const Currency.ada(),
isEditMode: isEditMode,
onChanged: onChanged,
);
case SingleLineTextEntryDefinition():
case MultiLineTextEntryDefinition():
final castProperty = definition.castProperty(property);
return SimpleTextEntryWidget(
property: castProperty as DocumentProperty<String>,
isEditMode: isEditMode,
onChanged: onChanged,
);
case YesNoChoiceDefinition():
final castProperty = definition.castProperty(property);
return YesNoChoiceWidget(
Expand Down
Loading