parseWidgetTree function

Future<List<Map<String, dynamic>>> parseWidgetTree(
  1. Element element
)

Parses the Flutter widget tree and returns a list of widget data maps.

element: The root element of the widget tree to parse.

Implementation

Future<List<Map<String, dynamic>>> parseWidgetTree(Element element) async {
  final widgetTree = <Map<String, dynamic>>[];
  final List<AccessiblePosition?> accessiblePositionList = [];
  AccessiblePosition? accessibility;

  /// All controls excluding the type 10 root node
  final List<Map<String, dynamic>> allControlsList = [];

  // Element parentElement;

  try {
    // Recursively parse the widget tree
    void traverse(Element element, [int depth = 0]) {
      final widget = element.widget;
      final type = widget.runtimeType.toString();

      /// Build type 10 object
      if (widget is Semantics ||
          widget is TextField ||
          widget is Text ||
          widget is ElevatedButton ||
          widget is TextFormField ||
          widget is TextField ||
          widget is Checkbox ||
          widget is CheckboxListTile ||
          widget is Switch ||
          widget is SwitchListTile ||
          widget is Slider ||
          widget is Radio ||
          widget is RadioListTile ||
          widget is DropdownButton ||
          widget is DropdownMenuItem ||
          widget is AlertDialog ||
          widget is SnackBar ||
          widget is Image ||
          widget is Icon) {
        RenderBox? renderObject = element.renderObject as RenderBox?;

        if (renderObject != null && renderObject.hasSize) {
          // Access properties or methods specific to RenderBox

          // final renderObject = element.renderObject as RenderBox;
          final position = renderObject.localToGlobal(Offset.zero);
          final size = renderObject.size;

          Map<String, dynamic>? aStyle;
          Map<String, dynamic>? font;
          Map<String, dynamic>? image;
          String? text = "";

          if (widget is Text) {
            final TextStyle style = widget.style ?? TextStyle();
            final TextAlign align = widget.textAlign ?? TextAlign.left;

            Widget currentWidget = widget;
            Padding padding;

            font = {
              'family': style.fontFamily,
              'size': style.fontSize.toString(),
              'bold': (style.fontWeight != null &&
                      FontWeight.values.indexOf(style.fontWeight!) >
                          FontWeight.values.indexOf(FontWeight.normal))
                  .toString(),
              'italic': (style.fontStyle == FontStyle.italic).toString()
            };

            double top = 0, bottom = 0, left = 0, right = 0;

            /// Get Padding
            element.visitAncestorElements((ancestor) {
              currentWidget = ancestor.widget;
              if (currentWidget is Padding) {
                padding = currentWidget as Padding;

                if (padding.padding is EdgeInsets) {
                  final EdgeInsets eig = padding.padding as EdgeInsets;
                  top = eig.top;
                  bottom = eig.bottom;
                  left = eig.left;
                  right = eig.right;
                }
                return false;
              }
              return true;
            });

            aStyle = {
              'textColor': ((style.color?.value ?? 0) & 0xFFFFFF).toString(),
              'textAlphaColor': (style.color?.alpha ?? 0).toString(),
              'textAlphaBGColor':
                  (style.backgroundColor?.alpha ?? 0).toString(),
              'textAlign': align.toString().split('.').last,
              'paddingBottom': bottom.toInt().toString(),
              'paddingTop': top.toInt().toString(),
              'paddingLeft': left.toInt().toString(),
              'paddingRight': right.toInt().toString(),
              'hidden': (style.color?.opacity == 1.0).toString(),
              'colorPrimary': (style.foreground?.color ?? 0).toString(),
              'colorPrimaryDark': 0.toString(), // TBD: Dark theme??
              'colorAccent': (style.decorationColor?.value ?? 0).toString(),
            };
          }

          /// Get Semantics
          if (widget is Semantics) {
            final Semantics semantics = widget;

            if (semantics.properties.label?.isNotEmpty == true ||
                semantics.properties.label?.isNotEmpty == true) {
              final String? hint = semantics.properties.hint;
              final String? label = semantics.properties.label;

              print(
                  'Tealeaf - Widget is a semantic type: ${semantics.properties}');

              /// Get Accessibility object, and its position for masking purpose
              accessibility = AccessiblePosition(
                id: element.toStringShort(),
                label: label ?? '',
                hint: hint ?? '',
                dx: position.dx,
                dy: position.dy,
                width: size.width,
                height: size.height,
              );
              accessiblePositionList.add(accessibility);
            }
          } else {
            text = widget is Text ? widget.data : '';
            final widgetData = {
              'type': type,
              'text': text,
              'position':
                  'x: ${position.dx}, y: ${position.dy}, width: ${size.width}, height: ${size.height}',
            };

            // tlLogger.v('WidgetData - ${widget.toString()}');

            widgetTree.add(widgetData);

            Map<String, dynamic> accessibilityMap = {
              'id': accessibility?.id,
              'label': accessibility?.label,
              'hint': accessibility?.hint,
            };

            final masked = (accessibility != null) ? true : false;
            final widgetId =
                widget.runtimeType.toString() + widget.hashCode.toString();

            /// Add the control as map to the list
            allControlsList.add(<String, dynamic>{
              'id': widgetId,
              'cssId': widgetId,
              'idType': (-4).toString(),
              // ignore: unnecessary_null_comparison
              'tlType': (image != null)
                  ? 'image'
                  : (text != null && text.contains('\n')
                      ? 'textArea'
                      : 'label'),
              'type': type,
              'subType': widget.runtimeType.toString(),
              'position': <String, String>{
                'x': position.dx.toInt().toString(),
                'y': position.dy.toInt().toString(),
                'width': renderObject.size.width.toInt().toString(),
                'height': renderObject.size.height.toInt().toString(),
              },
              'zIndex': "501",
              'currState': <String, dynamic>{'text': text, 'font': font},
              if (aStyle != null) 'style': aStyle,
              if (accessibility != null) 'accessibility': accessibilityMap,
              'originalId': "",
              'masked': '$masked'
            });

            /// Reset
            if (accessibility != null) {
              accessibility = null;
            }
          }
        }
      }

      /// Recursively call to wall down the tree, only Visible children
      element.visitChildren((child) {
        bool visible = true;
        if (widget is Visibility) {
          final visibility = widget;
          if (!visibility.visible) {
            visible = false;
          }
        }

        /// Skip invisible Widgets
        if (visible) {
          // parentElement = element;
          // tlLogger.v('Parent widget - $parentElement.');

          traverse(child, depth + 1);
        }
        return;
      });
    }

    /// Starting to parse tree
    traverse(element, 0);

    // Encode the JSON object
    String jsonString = jsonEncode(widgetTree);

    PluginTealeaf.tlApplicationCustomEvent(eventName: jsonString);
  } catch (error) {
    // Handle errors using try-catch block
    tlLogger.v('Error caught in try-catch: $error');
  }
  return allControlsList;
}