Html – Displaying HTML node in tree – height issue

apache-flexhtml

Grrr so close yet still failing…

I display this tree, in Flex, which contains two nodes types:

  1. Regular nodes, which are rendered normally like text (because they are!)
  2. Rich (HTML) nodes – that's where things get twisted

Note that my issue is when I dynamically add a new (HTML) node to my tree.


So… How do I display HTML nodes?

  1. I subclass TreeItemRenderer
  2. In that subclass, I override set data() and add a text child to my renderer

Therefore I now have:

[icon] [label]

[text component]

Why?

The default label is a pure text component, not HTML-capable, hence the extra component: I want to display the new guy and forget the default label.

  1. (continued) I override updateDisplayList() and, if the node is a rich one, I set label's height to zero, set my component's x and y to label'x and and y.

So…what am I missing? Ah, yes: I need to set my node's height since HTML text can be bigger or smaller than its text counterpart.

  1. (continued) I override measure()
  2. If my node is not a rich one, I simply invoke super.measure() and return
  3. If it is a rich one, I give my html component a width (htmlComponent.width = explicitWidth – super.label.x;) and its height should be automatically computed.

This gives me a fairly reliably unreliable result!

When I fold/unfold my tree, every other time, I seem to get a correct height for my HTML node. The other time I get a height of '4' which happens to be the HTML component's padding alone, without content.

I know that I must be doing something fairly stupid here…but I am not sure what. I will post my code if my rambling is too incoherent to make any sense of…

**** EDIT: here is the source code for my renderer
As you can see, only 'notes' nodes use HTML.
I add a 'htmlComponent' child that will display the rich text while the default label is zero-sized and disappears.
It's definitely very raw code, as it's in progress!

    package com.voilaweb.tfd
{
    import mx.collections.*;
    import mx.controls.Text;
    import mx.controls.treeClasses.*;
    import mx.core.UITextField;
    import mx.core.UIComponent;
    import flash.text.TextLineMetrics;

    public class OutlinerRenderer extends TreeItemRenderer
    {
        private function get is_note():Boolean
        {
            return ('outlinerNodeNote' == XML(super.data).name().localName);
        }

        override public function set data(value:Object):void
        {
            super.data = value;
            var htmlComponent:Text = super.getChildByName("htmlComponent") as Text;
            if(!htmlComponent)
            {
                htmlComponent = new Text();
                htmlComponent.name = "htmlComponent";
                addChild(htmlComponent);
            }
            if(is_note)
                htmlComponent.htmlText = XML(super.data).attribute('nodeText');
            else
                htmlComponent.htmlText = null;
            setStyle('verticalAlign', 'top');
        }

 /*
  * Today we've learnt a valuable lesson: there is no guarantee of when createChildren() will be invoked.
  * Better be dirty and add children in set data()
        override protected function createChildren():void
        {
            super.createChildren();
            var htmlComponent:Text = new Text();
            htmlComponent.name = "htmlComponent";
            addChild(htmlComponent);
        }
*/
        override protected function measure():void
        {
          if(is_note)
          {
            super.measure();
            var htmlComponent:Text = super.getChildByName("htmlComponent") as Text;
            //Setting the width of the description field
            //causes the height calculation to happen
            htmlComponent.width = explicitWidth - super.label.x;
            //We add the measuredHeight to the renderers measured height
            //measuredHeight += (htmlComponent.measuredHeight - label.measuredHeight);
            // Note the silly trick here...hopefully in the future I figure out how to avoid it
            //
            // Here is what happens: we check if measuredHeight is equal to decoration such as margin, insets...rather than that + some height
            // If so, then we need to come up with an actual height which we do by adding textHeight to this height

            // Note that I care about text being equal to margin etc but do not have proper access to these
            // For instance UITextField.TEXT_HEIGHT_PADDING == 4 but is not accessible
            // I am going to check if "<10" that will cover this case...
            trace("For text " + htmlComponent.htmlText);
            trace("width = " + htmlComponent.getExplicitOrMeasuredWidth()+" x height = " + htmlComponent.getExplicitOrMeasuredHeight());
            var m:TextLineMetrics = htmlComponent.measureHTMLText(htmlComponent.htmlText);
            //if(10 > htmlComponent.measuredHeight && !isNaN(htmlComponent.explicitHeight))
            //htmlComponent.explicitHeight = m.height + htmlComponent.measuredHeight;
            //if(htmlComponent.measuredHeight < 10) htmlComponent.explicitHeight = 50;

            //measuredHeight += (htmlComponent.getExplicitOrMeasuredHeight() - super.label.getExplicitOrMeasuredHeight());
            measuredHeight += (htmlComponent.getExplicitOrMeasuredHeight() - label.getExplicitOrMeasuredHeight());
            trace("m:"+m.height+" Height: " + htmlComponent.getExplicitOrMeasuredHeight());
          }
          else
          {
            super.measure();
          }
        }     

        override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
        {
            super.updateDisplayList(unscaledWidth, unscaledHeight);
            label.height = label.getExplicitOrMeasuredHeight(); // If you tell me my height, then I shall use my variable height!

            graphics.clear();

            if(is_note)
            {
                label.height = 0;
                var htmlComponent:Text = super.getChildByName("htmlComponent") as Text;
                htmlComponent.x = label.x;
                htmlComponent.y = label.y;
                htmlComponent.height = htmlComponent.getExplicitOrMeasuredHeight();

                graphics.beginFill(0x555555);
                graphics.drawRect(0, 0, unscaledWidth, unscaledHeight);
                graphics.endFill();
            }

            var complete:XMLList = XML(super.data).attribute('complete');
            if(complete.length() > 0 && true == complete[0])
            {
                var startx:Number = data ? TreeListData(listData).indent : 0;
                if(disclosureIcon)
                    startx += disclosureIcon.measuredWidth;
                if(icon)
                    startx += icon.measuredWidth;
                graphics.lineStyle(3, getStyle("color"));
                var y:Number = label.y + label.getExplicitOrMeasuredHeight() / 2;
                graphics.moveTo(startx, y);
                graphics.lineTo(startx + label.getExplicitOrMeasuredWidth(), y);
            }
        }
    }
}

Best Solution

You made false assumption about label component in default renderer - it is capable of displaying html content. This renderer works for me:

public class HtmlTreeItemRenderer extends TreeItemRenderer {
    override protected function commitProperties():void {
        super.commitProperties();
        label.htmlText = data ? listData.label : "";
        invalidateDisplayList();
    }
}