From 698f058da60aeb05ebf4025ce28a96c063543108 Mon Sep 17 00:00:00 2001 From: Sofanyas Genene <123987957+Sofanyas@users.noreply.github.com> Date: Thu, 24 Apr 2025 09:54:02 -0400 Subject: [PATCH] refactor: allow users to edit morphs from word zoom widget Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: ggurdin Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com> --- lib/pangea/morphs/edit_morph_widget.dart | 233 +++++++++++++++ .../reading_assistance_input_bar.dart | 2 - .../word_zoom_activity_button.dart | 45 +-- .../widgets/reading_assistance_content.dart | 5 + .../widgets/word_zoom/morph_focus_widget.dart | 272 +++--------------- .../word_zoom/morphological_list_item.dart | 12 + .../widgets/word_zoom/word_zoom_widget.dart | 115 +++++--- 7 files changed, 382 insertions(+), 302 deletions(-) create mode 100644 lib/pangea/morphs/edit_morph_widget.dart diff --git a/lib/pangea/morphs/edit_morph_widget.dart b/lib/pangea/morphs/edit_morph_widget.dart new file mode 100644 index 000000000..ee471af8a --- /dev/null +++ b/lib/pangea/morphs/edit_morph_widget.dart @@ -0,0 +1,233 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/pangea/common/constants/model_keys.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart'; +import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; +import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; +import 'package:fluffychat/pangea/morphs/morph_repo.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; + +class EditMorphWidget extends StatefulWidget { + final PangeaToken token; + final PangeaMessageEvent pangeaMessageEvent; + final MorphFeaturesEnum morphFeature; + final VoidCallback onClose; + + const EditMorphWidget({ + required this.token, + required this.pangeaMessageEvent, + required this.morphFeature, + required this.onClose, + super.key, + }); + + @override + State createState() => EditMorphWidgetState(); +} + +class EditMorphWidgetState extends State { + List? _availableMorphTags; + String? _selectedMorphTag; + + @override + void initState() { + super.initState(); + _setAvailableMorphs(widget.morphFeature); + _selectedMorphTag = _assignedMorphTag; + } + + @override + void didUpdateWidget(covariant EditMorphWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.morphFeature != widget.morphFeature) { + _setAvailableMorphs(widget.morphFeature); + } + } + + String? get _assignedMorphTag => widget.token.morph[widget.morphFeature]; + + Future _setAvailableMorphs(MorphFeaturesEnum feature) async { + try { + setState(() => _availableMorphTags = null); + final resp = await MorphsRepo.get(); + _availableMorphTags = resp.getDisplayTags( + feature.name, + ); + } catch (e) { + _availableMorphTags = defaultMorphMapping.getDisplayTags( + feature.name, + ); + } finally { + if (mounted) setState(() {}); + } + } + + void _saveChanges() { + if (_selectedMorphTag == null) return; + showFutureLoadingDialog( + context: context, + future: () => _sendEditedMessage( + (token) { + token.morph[widget.morphFeature] = _selectedMorphTag!; + if (widget.morphFeature.name.toLowerCase() == 'pos') { + token.pos = _selectedMorphTag!; + } + return token; + }, + ), + ); + } + + Future _sendEditedMessage( + PangeaToken Function(PangeaToken token) changeCallback, + ) async { + try { + final pm = widget.pangeaMessageEvent; + final existingTokens = pm.originalSent!.tokens! + .map((token) => PangeaToken.fromJson(token.toJson())) + .toList(); + + final tokenIndex = existingTokens.indexWhere( + (token) => token.text.offset == widget.token.text.offset, + ); + if (tokenIndex == -1) { + throw Exception("Token not found in message"); + } + existingTokens[tokenIndex] = changeCallback(existingTokens[tokenIndex]); + + await pm.room.pangeaSendTextEvent( + pm.messageDisplayText, + editEventId: pm.eventId, + originalSent: pm.originalSent?.content, + originalWritten: pm.originalWritten?.content, + tokensSent: PangeaMessageTokens( + tokens: existingTokens, + detections: pm.originalSent?.detections, + ), + tokensWritten: pm.originalWritten?.tokens != null + ? PangeaMessageTokens( + tokens: pm.originalWritten!.tokens!, + detections: pm.originalWritten?.detections, + ) + : null, + choreo: pm.originalSent?.choreo, + messageTag: ModelKey.messageTagMorphEdit, + ); + + widget.onClose(); + } catch (e) { + ErrorHandler.logError( + e: e, + data: { + "selectedMorphTag": _selectedMorphTag, + "morphFeature": widget.morphFeature.name, + "pangeaMessageEvent": widget.pangeaMessageEvent.event.content, + }, + ); + } + } + + bool get _canSaveChanges => + _selectedMorphTag != _assignedMorphTag && _selectedMorphTag != null; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + spacing: 8.0, + children: [ + Text( + "${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).chooseCorrectLabel}", + textAlign: TextAlign.center, + style: const TextStyle(fontStyle: FontStyle.italic), + ), + if (_availableMorphTags == null || _availableMorphTags!.isEmpty) + const CircularProgressIndicator() + else + Wrap( + alignment: WrapAlignment.center, + children: _availableMorphTags!.map((tag) { + return Container( + margin: const EdgeInsets.all(2), + padding: EdgeInsets.zero, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(10), + ), + border: Border.all( + color: _selectedMorphTag == tag + ? Theme.of(context).colorScheme.primary + : Colors.transparent, + style: BorderStyle.solid, + width: 2.0, + ), + ), + child: TextButton( + style: ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric( + horizontal: 7, + ), + ), + backgroundColor: WidgetStateProperty.all( + _selectedMorphTag == tag + ? Theme.of(context).colorScheme.primary.withAlpha(50) + : Colors.transparent, + ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + onPressed: () => setState(() => _selectedMorphTag = tag), + child: Text( + getGrammarCopy( + category: widget.morphFeature.name, + lemma: tag, + context: context, + ) ?? + tag, + textAlign: TextAlign.center, + ), + ), + ); + }).toList(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 10, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 10), + ), + onPressed: widget.onClose, + child: Text(L10n.of(context).cancel), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + padding: const EdgeInsets.symmetric(horizontal: 10), + ), + onPressed: _canSaveChanges ? _saveChanges : null, + child: Text(L10n.of(context).saveChanges), + ), + ], + ), + ], + ); + } +} diff --git a/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart b/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart index 58f1a9d0b..df62c8944 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart @@ -86,11 +86,9 @@ class ReadingAssistanceInputBar extends StatelessWidget { ); } else if (overlayController.selectedMorph != null) { content = MorphFocusWidget( - token: overlayController.selectedMorph!.token, morphFeature: overlayController.selectedMorph!.morph, pangeaMessageEvent: overlayController.pangeaMessageEvent!, overlayController: overlayController, - onEditDone: () => overlayController.setState(() {}), ); } else { content = Center( diff --git a/lib/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart b/lib/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart index 6afd7a843..9628df870 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart @@ -4,7 +4,8 @@ class WordZoomActivityButton extends StatelessWidget { final Widget icon; final bool isSelected; final VoidCallback onPressed; - + final VoidCallback? onDoubleTap; + final VoidCallback? onLongPress; final String? tooltip; final double? opacity; @@ -12,6 +13,8 @@ class WordZoomActivityButton extends StatelessWidget { required this.icon, required this.isSelected, required this.onPressed, + this.onDoubleTap, + this.onLongPress, this.tooltip, this.opacity, super.key, @@ -22,25 +25,29 @@ class WordZoomActivityButton extends StatelessWidget { Widget buttonContent = AnimatedSize( duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, - child: IconButton( - onPressed: onPressed, - icon: AnimatedBuilder( - animation: Listenable.merge([ValueNotifier(isSelected)]), - builder: (context, child) { - return Transform.scale( - scale: isSelected ? 1.25 : 1.0, - child: icon, - ); - }, + child: GestureDetector( + onDoubleTap: onDoubleTap, + onLongPress: onLongPress, + child: IconButton( + onPressed: onPressed, + icon: AnimatedBuilder( + animation: Listenable.merge([ValueNotifier(isSelected)]), + builder: (context, child) { + return Transform.scale( + scale: isSelected ? 1.25 : 1.0, + child: icon, + ); + }, + ), + iconSize: 24, // Keep this constant as scaling handles the size change + color: isSelected ? Theme.of(context).colorScheme.primary : null, + visualDensity: VisualDensity.compact, + // style: IconButton.styleFrom( + // backgroundColor: isSelected + // ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.25) + // : Colors.transparent, + // ), ), - iconSize: 24, // Keep this constant as scaling handles the size change - color: isSelected ? Theme.of(context).colorScheme.primary : null, - visualDensity: VisualDensity.compact, - // style: IconButton.styleFrom( - // backgroundColor: isSelected - // ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.25) - // : Colors.transparent, - // ), ), ); diff --git a/lib/pangea/toolbar/widgets/reading_assistance_content.dart b/lib/pangea/toolbar/widgets/reading_assistance_content.dart index facdb86e5..01c249418 100644 --- a/lib/pangea/toolbar/widgets/reading_assistance_content.dart +++ b/lib/pangea/toolbar/widgets/reading_assistance_content.dart @@ -8,6 +8,7 @@ import 'package:matrix/matrix_api_lite/model/message_types.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; @@ -38,6 +39,8 @@ class ReadingAssistanceContent extends StatefulWidget { } class ReadingAssistanceContentState extends State { + MorphFeaturesEnum? _selectedEditMorphFeature; + TtsController get ttsController => widget.overlayController.widget.chatController.choreographer.tts; @@ -125,6 +128,8 @@ class ReadingAssistanceContentState extends State { messageEvent: widget.overlayController.pangeaMessageEvent!, tts: ttsController, overlayController: widget.overlayController, + editMorph: (m) => setState(() => _selectedEditMorphFeature = m), + selectedEditMorphFeature: _selectedEditMorphFeature, ); } } diff --git a/lib/pangea/toolbar/widgets/word_zoom/morph_focus_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/morph_focus_widget.dart index 4747da0e2..9e196c453 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/morph_focus_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/morph_focus_widget.dart @@ -8,38 +8,27 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart'; import 'package:fluffychat/pangea/analytics_details_popup/morph_meaning_widget.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; -import 'package:fluffychat/pangea/common/constants/model_keys.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/lemmas/construct_xp_widget.dart'; -import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart'; -import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; +import 'package:fluffychat/pangea/morphs/edit_morph_widget.dart'; import 'package:fluffychat/pangea/morphs/morph_feature_display.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; -import 'package:fluffychat/pangea/morphs/morph_repo.dart'; import 'package:fluffychat/pangea/morphs/morph_tag_display.dart'; +import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_selection.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; class MorphFocusWidget extends StatefulWidget { - final PangeaToken token; final MorphFeaturesEnum morphFeature; final PangeaMessageEvent pangeaMessageEvent; final MessageOverlayController overlayController; - final VoidCallback onEditDone; - const MorphFocusWidget({ - required this.token, required this.morphFeature, required this.pangeaMessageEvent, required this.overlayController, - required this.onEditDone, super.key, }); @@ -48,36 +37,32 @@ class MorphFocusWidget extends StatefulWidget { } class MorphFocusWidgetState extends State { - bool editMode = false; + PangeaToken get token => widget.overlayController.selectedToken!; + + bool _editMode = false; /// the morphological tag that the user has selected in edit mode - String selectedMorphTag = ""; - - List morphFeatures = []; + String _selectedMorphTag = ""; final ScrollController _scrollController = ScrollController(); - void resetMorphTag() { + void _resetMorphTag() { setState( - () => selectedMorphTag = - widget.token.getMorphTag(widget.morphFeature) ?? "X", + () => _selectedMorphTag = token.getMorphTag(widget.morphFeature) ?? "X", ); } @override void initState() { super.initState(); - resetMorphTag(); - _setAvailableMorphs(); + _resetMorphTag(); } @override void didUpdateWidget(MorphFocusWidget oldWidget) { - if (widget.token != oldWidget.token || - widget.morphFeature != oldWidget.morphFeature) { - resetMorphTag(); - _setAvailableMorphs(); - setState(() => editMode = false); + if (widget.morphFeature != oldWidget.morphFeature) { + _resetMorphTag(); + setState(() => _editMode = false); } super.didUpdateWidget(oldWidget); } @@ -88,98 +73,15 @@ class MorphFocusWidgetState extends State { super.dispose(); } - void enterEditMode() { + void _enterEditMode() { setState(() { - editMode = true; + _editMode = true; }); } - PangeaMessageEvent get pm => widget.pangeaMessageEvent; - - Future _setAvailableMorphs() async { - try { - final resp = await MorphsRepo.get(); - morphFeatures = resp.getDisplayTags( - widget.morphFeature.name, - ); - } catch (e) { - morphFeatures = defaultMorphMapping.getDisplayTags( - widget.morphFeature.name, - ); - } finally { - if (mounted) setState(() {}); - } - } - - /// confirm the changes made by the user - /// this will send a new message to the server - /// with the new morphological tag - Future saveChanges( - PangeaToken Function(PangeaToken token) changeCallback, - ) async { - try { - // NOTE: it is not clear how this would work if the user was not editing the originalSent tokens - // this case would only happen in immersion mode which is disabled until further notice - // this flow assumes that the user is editing the originalSent tokens - // if not, we'll get an error and we'll cross that bridge - - // make a copy of the original tokens - final existingTokens = pm.originalSent!.tokens! - .map((token) => PangeaToken.fromJson(token.toJson())) - .toList(); - - // change the morphological tag in the selected token - final tokenIndex = existingTokens - .indexWhere((token) => token.text.offset == widget.token.text.offset); - if (tokenIndex == -1) { - throw Exception("Token not found in message"); - } - existingTokens[tokenIndex] = changeCallback(existingTokens[tokenIndex]); - - // send a new message as an edit to original message to the server - // including the new tokens - // marking the message as a morphological edit will allow use to filter - // from some processing and potentially find the data for LLM fine-tuning - await pm.room.pangeaSendTextEvent( - pm.messageDisplayText, - editEventId: pm.eventId, - originalSent: pm.originalSent?.content, - originalWritten: pm.originalWritten?.content, - tokensSent: PangeaMessageTokens( - tokens: existingTokens, - detections: pm.originalSent?.detections, - ), - tokensWritten: pm.originalWritten?.tokens != null - ? PangeaMessageTokens( - tokens: pm.originalWritten!.tokens!, - detections: pm.originalWritten?.detections, - ) - : null, - choreo: pm.originalSent?.choreo, - messageTag: ModelKey.messageTagMorphEdit, - ); - - setState(() => editMode = false); - widget.onEditDone(); - } catch (e) { - SnackBar( - content: Text(L10n.of(context).oopsSomethingWentWrong), - ); - ErrorHandler.logError( - e: e, - data: { - "selectedMorphTag": selectedMorphTag, - "morphFeature": widget.morphFeature, - "token": widget.token.toJson(), - "pangeaMessageEvent": widget.pangeaMessageEvent.event.content, - }, - ); - } - } - - ConstructIdentifier get id { + ConstructIdentifier get _id { return ConstructIdentifier( - lemma: selectedMorphTag, + lemma: _selectedMorphTag, type: ConstructTypeEnum.morph, category: widget.morphFeature.name, ); @@ -187,7 +89,7 @@ class MorphFocusWidgetState extends State { @override Widget build(BuildContext context) { - if (!editMode) { + if (!_editMode) { return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, @@ -196,7 +98,7 @@ class MorphFocusWidgetState extends State { MorphFeatureDisplay( morphFeature: widget.morphFeature, ), - if (widget.token.getMorphTag(widget.morphFeature) != null) ...[ + if (token.getMorphTag(widget.morphFeature) != null) ...[ Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, @@ -205,26 +107,26 @@ class MorphFocusWidgetState extends State { triggerMode: TooltipTriggerMode.tap, message: L10n.of(context).doubleClickToEdit, child: GestureDetector( - onLongPress: enterEditMode, - onDoubleTap: enterEditMode, + onLongPress: _enterEditMode, + onDoubleTap: _enterEditMode, child: MorphTagDisplay( morphFeature: widget.morphFeature, - morphTag: widget.token.getMorphTag(widget.morphFeature) ?? + morphTag: token.getMorphTag(widget.morphFeature) ?? L10n.of(context).nan, textColor: Theme.of(context).brightness == Brightness.light - ? id.constructUses.lemmaCategory.darkColor(context) - : id.constructUses.lemmaCategory.color(context), + ? _id.constructUses.lemmaCategory.darkColor(context) + : _id.constructUses.lemmaCategory.color(context), ), ), ), const SizedBox(width: 6), ConstructXpWidget( - id: id, + id: _id, onTap: () => showDialog( context: context, builder: (context) => AnalyticsPopupWrapper( - constructZoom: id, + constructZoom: _id, view: ConstructTypeEnum.morph, ), ), @@ -233,7 +135,7 @@ class MorphFocusWidgetState extends State { ), MorphMeaningWidget( feature: widget.morphFeature, - tag: widget.token.getMorphTag(widget.morphFeature)!, + tag: token.getMorphTag(widget.morphFeature)!, style: Theme.of(context).textTheme.bodyLarge, ), ] else @@ -244,117 +146,19 @@ class MorphFocusWidgetState extends State { return Padding( padding: const EdgeInsets.all(4.0), - child: Column( - children: [ - Text( - "${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).chooseCorrectLabel}", - textAlign: TextAlign.center, - style: const TextStyle(fontStyle: FontStyle.italic), - ), - if (morphFeatures.isEmpty) - const CircularProgressIndicator() - else - Wrap( - children: morphFeatures.map((tag) { - return Container( - margin: const EdgeInsets.all(2), - padding: EdgeInsets.zero, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(10), - ), - border: Border.all( - color: selectedMorphTag == tag - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - style: BorderStyle.solid, - width: 2.0, - ), - ), - child: TextButton( - style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric( - horizontal: 7, - ), - ), - backgroundColor: WidgetStateProperty.all( - selectedMorphTag == tag - ? Theme.of(context) - .colorScheme - .primary - .withAlpha(50) - : Colors.transparent, - ), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ), - onPressed: () { - setState(() => selectedMorphTag = tag); - }, - child: Text( - getGrammarCopy( - category: widget.morphFeature.name, - lemma: tag, - context: context, - ) ?? - tag, - textAlign: TextAlign.center, - ), - ), - ); - }).toList(), + child: EditMorphWidget( + token: token, + pangeaMessageEvent: widget.pangeaMessageEvent, + morphFeature: widget.morphFeature, + onClose: () { + setState(() => _editMode = false); + widget.overlayController.onMorphActivitySelect( + MorphSelection( + token, + widget.morphFeature, ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 10, - children: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - padding: const EdgeInsets.symmetric(horizontal: 10), - ), - onPressed: () { - setState(() { - editMode = false; - }); - }, - child: Text(L10n.of(context).cancel), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - padding: const EdgeInsets.symmetric(horizontal: 10), - ), - onPressed: - selectedMorphTag == widget.token.morph[widget.morphFeature] - ? null - : () => showFutureLoadingDialog( - context: context, - future: () => saveChanges( - (token) { - token.morph[widget.morphFeature] = - selectedMorphTag; - if (widget.morphFeature.name.toLowerCase() == - 'pos') { - token.pos = selectedMorphTag; - } - return token; - }, - ), - ), - child: Text(L10n.of(context).saveChanges), - ), - ], - ), - ], + ); + }, ), ); } diff --git a/lib/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart b/lib/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart index a88333e85..b26d446b9 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart @@ -16,11 +16,13 @@ class MorphologicalListItem extends StatelessWidget { final MorphFeaturesEnum morphFeature; final PangeaToken token; final MessageOverlayController overlayController; + final VoidCallback editMorph; const MorphologicalListItem({ required this.morphFeature, required this.token, required this.overlayController, + required this.editMorph, super.key, }); @@ -55,6 +57,16 @@ class MorphologicalListItem extends StatelessWidget { isSelected: isSelected, onPressed: () => overlayController .onMorphActivitySelect(MorphSelection(token, morphFeature)), + onLongPress: () { + overlayController + .onMorphActivitySelect(MorphSelection(token, morphFeature)); + editMorph(); + }, + onDoubleTap: () { + overlayController + .onMorphActivitySelect(MorphSelection(token, morphFeature)); + editMorph(); + }, tooltip: shouldDoActivity ? morphFeature.getDisplayCopy(context) : getGrammarCopy( diff --git a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart index 836cd974d..edaa128cd 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/lemmas/construct_xp_widget.dart'; import 'package:fluffychat/pangea/lemmas/lemma_emoji_row.dart'; +import 'package:fluffychat/pangea/morphs/edit_morph_widget.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; @@ -24,6 +25,9 @@ class WordZoomWidget extends StatelessWidget { final PangeaMessageEvent messageEvent; final TtsController tts; final MessageOverlayController overlayController; + final Function(MorphFeaturesEnum?) editMorph; + + final MorphFeaturesEnum? selectedEditMorphFeature; const WordZoomWidget({ super.key, @@ -31,11 +35,13 @@ class WordZoomWidget extends StatelessWidget { required this.messageEvent, required this.tts, required this.overlayController, + required this.editMorph, + required this.selectedEditMorphFeature, }); PangeaToken get _selectedToken => overlayController.selectedToken!; - void onEditDone() => overlayController.initializeTokensAndMode(); + void _onEditDone() => overlayController.initializeTokensAndMode(); bool get hasEmojiActivity => overlayController.practiceSelection?.hasActiveActivityByToken( @@ -83,7 +89,7 @@ class WordZoomWidget extends StatelessWidget { }, onEditDone: () { debugPrint("what are we doing edits with?"); - onEditDone(); + _onEditDone(); }, tts: tts, overlayController: overlayController, @@ -104,56 +110,68 @@ class WordZoomWidget extends StatelessWidget { const SizedBox( height: 8.0, ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - constraints: const BoxConstraints( - minHeight: 40, - ), - alignment: Alignment.center, - child: LemmaEmojiRow( - cId: _selectedToken.vocabConstructID, - onTapOverride: overlayController.hideWordCardContent && - hasEmojiActivity - ? () => overlayController.updateToolbarMode( - MessageMode.wordEmoji, - ) - : null, - isSelected: - overlayController.toolbarMode == MessageMode.wordEmoji, - emojiSetCallback: () => overlayController.setState(() {}), - shouldShowEmojis: !hasEmojiActivity, - ), - ), - ], - ), - const SizedBox( - height: 8.0, - ), - Container( - constraints: const BoxConstraints( - minHeight: 40, - ), - alignment: Alignment.center, - child: Wrap( - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 8, + if (selectedEditMorphFeature == null) + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - LemmaMeaningWidget( - constructUse: token.vocabConstructID.constructUses, - langCode: MatrixState.pangeaController.languageController - .userL2?.langCodeShort ?? - LanguageKeys.defaultLanguage, - token: overlayController.selectedToken!, - controller: overlayController, - style: Theme.of(context).textTheme.bodyLarge, + Container( + constraints: const BoxConstraints( + minHeight: 40, + ), + alignment: Alignment.center, + child: LemmaEmojiRow( + cId: _selectedToken.vocabConstructID, + onTapOverride: overlayController.hideWordCardContent && + hasEmojiActivity + ? () => overlayController.updateToolbarMode( + MessageMode.wordEmoji, + ) + : null, + isSelected: overlayController.toolbarMode == + MessageMode.wordEmoji, + emojiSetCallback: () => overlayController.setState(() {}), + shouldShowEmojis: !hasEmojiActivity, + ), ), ], ), + const SizedBox( + height: 8.0, ), + if (selectedEditMorphFeature == null) + Container( + constraints: const BoxConstraints( + minHeight: 40, + ), + alignment: Alignment.center, + child: Wrap( + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 8, + children: [ + LemmaMeaningWidget( + constructUse: token.vocabConstructID.constructUses, + langCode: MatrixState.pangeaController.languageController + .userL2?.langCodeShort ?? + LanguageKeys.defaultLanguage, + token: overlayController.selectedToken!, + controller: overlayController, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ) + else + EditMorphWidget( + token: token, + pangeaMessageEvent: overlayController.pangeaMessageEvent!, + morphFeature: selectedEditMorphFeature!, + onClose: () { + editMorph(null); + overlayController.setState(() {}); + }, + ), const SizedBox( height: 8.0, ), @@ -196,6 +214,9 @@ class WordZoomWidget extends StatelessWidget { ), token: _selectedToken, overlayController: overlayController, + editMorph: () => editMorph( + MorphFeaturesEnumExtension.fromString(cId.category), + ), ), ), ],