środa, 20 kwietnia 2011

Flex 4: Truncate Label in the middle

Flex offers the possibility to truncate Label's content (both Spark and MX) at the end and add "..." in place of truncated text.

I made a Spark component that derives from Label and truncates text in the middle. It's just a re-make based strongly on it's MX version:

http://cookbooks.adobe.com/post_Graphical_truncating_of_texts_and_labels__custom_t-13306.html

Here's the code for Spark version:

package
{
    import flash.text.TextField;
   
    import mx.core.UITextField;
    import mx.core.UITextFormat;
    import mx.core.mx_internal;
    import mx.managers.ISystemManager;
    import spark.components.Label;
    use namespace mx_internal;
   
    public class MiddleTruncatingLabel extends Label
    {
        public function MiddleTruncatingLabel()
        {
        }
       
        private var _trueText:String;
        private var _textChanged:Boolean = false;
        private var _isTruncated:Boolean = false;
       
        override public function set text(value:String):void
        {
            // Check if value changed and indicate that
            // to initiate new middle-truncating
            // Remember original text
            if (value != _trueText)
            {
                _trueText = value;
                super.text = truncateTextMiddle(_trueText, unscaledWidth);
                _textChanged = true;
            }
        }
       
        override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
        {
            super.updateDisplayList(unscaledWidth, unscaledHeight);
           
            // check if need truncate and truncate
            if ( !isNaN(unscaledWidth) && _textChanged )
            {
                // override textfield's text
                text = truncateTextMiddle(_trueText, unscaledWidth);
               
                // update info to check if need truncate to do not run
                // middle-truncation too much of times
                _textChanged = false;
            }
        }
       
        public function truncateTextMiddle(fullText:String, widthToTruncate:Number) : String
        {
            if (!(fullText) || fullText.length < 3 || !this.parent)
            {
                // skip any truncating if no styles (no parent),
                // or text is too small
                return fullText;
            }
           
            // add paddings for some font oversize issues
            var paddingWidth:Number =
                UITextField.mx_internal::TEXT_WIDTH_PADDING +
                this.getStyle("paddingLeft") + this.getStyle("paddingRight");
           
            // Skip if width is too small
            if (widthToTruncate < paddingWidth + 10) return fullText;
           
            // Prepare measurement object
            // We create new TextField, and copy styles for it from this object
            // We cannot re-use internal original text field instance because
            // it will cause event firing in process of text measurement
            var measurementField:TextField = new TextField();
           
            // Clear so measured text will not get old styles.
            measurementField.text = "";
           
            // Copy styles into TextField
            var textStyles:UITextFormat = this.determineTextFormatFromStyles();
            measurementField.defaultTextFormat = textStyles;
            var sm:ISystemManager = this.systemManager;
            if (textStyles.font)
            {
                measurementField.embedFonts = sm != null && sm.isFontFaceEmbedded(textStyles);
            }
            else
            {
                measurementField.embedFonts = false;
            }
            if (textStyles.antiAliasType) {
                measurementField.antiAliasType = textStyles.antiAliasType;
            }
            if (textStyles.gridFitType) {
                measurementField.gridFitType = textStyles.gridFitType;
            }
            if (!isNaN(textStyles.sharpness)) {
                measurementField.sharpness = textStyles.sharpness;
            }
            if (!isNaN(textStyles.thickness)) {
                measurementField.thickness = textStyles.thickness;
            }
           
            // Perform initial measure of text and check if need truncating at all
           
            // To measure text, we set it to measurement text field
            // and get line metrics for first line
            measurementField.text = fullText;
            var fullTextWidth:Number = measurementField.getLineMetrics(0).width + paddingWidth;
            if(fullTextWidth > widthToTruncate){
                // get width of ...
                measurementField.text = "...";
                var dotsWidth:Number = measurementField.getLineMetrics(0).width;
               
                // Find out what is the half of truncated text without ...
                var halfWidth : Number = (widthToTruncate - paddingWidth - dotsWidth) / 2;
               
                // Make a rough estimate of how much chars we need to cut out
                // This saves steps of character-by-character preocessing
                measurementField.text = "s";
                var charWidth:Number = measurementField.getLineMetrics(0).width;
                var charsToTruncate:int = Math.round(
                    ((fullTextWidth - paddingWidth) / 2 - halfWidth) /
                    charWidth) + 2;
               
                // allow some distortion to account fractional widths part
                halfWidth = halfWidth - 0.5;
               
                // Below algorithm makes rough middle-truncating
                // Then it is corrected by adding or removing
                // characters for each part until reach required
                // width for each half. Algorith does checks
                // (min max and loop ciodnitions) so that string
                // cannot be less then one character for each half
               
                // see if right part of text approximately fits into half width
                var rightPart:String;
                var widthWithNextChar:Number;
               
                var len:int = fullText.length;
                var currLoc:int = Math.min(len/2 + charsToTruncate + 1, len-1);
                measurementField.text = fullText.substr(currLoc);
                var rightPartWidth:Number = measurementField.getLineMetrics(0).width;
               
                if (rightPartWidth > halfWidth) {
                    // throw away characters until fits
                    currLoc++;
                    while (rightPartWidth > halfWidth && currLoc < len) {
                        measurementField.text = fullText.charAt(currLoc);
                        rightPartWidth -= measurementField.getLineMetrics(0).width;
                        currLoc++;
                    }
                    rightPart = fullText.substr(currLoc - 1);
                } else {
                    // try to add characters one-by-one and
                    // see if it still fits
                    widthWithNextChar = 0;
                    do {
                        currLoc--;
                        rightPartWidth += widthWithNextChar;
                        measurementField.text = fullText.charAt(currLoc);
                        widthWithNextChar = measurementField.getLineMetrics(0).width;
                    } while (rightPartWidth + widthWithNextChar <= halfWidth && currLoc > 0);
                    rightPart = fullText.substr(currLoc + 1);
                }
               
                // Do the same with left part, but compare overall string
                // Overall is needed because character-by character
                // would not give us correct total width of string -
                // somehow overall text is measured with sapcers etc. and
                // also there are rounding issues.
                // This way, and by putting left part calculating as last, we allow
                // left part might be larger (may become more than half).
               
                // allow some distortion in widths fractions
                widthToTruncate = widthToTruncate - 0.5 - paddingWidth;
               
                currLoc = Math.max(len/2 - charsToTruncate, 1);
                measurementField.text = fullText.substr(0, currLoc) +
                    "..." + rightPart;
                var truncatedWidth:Number = measurementField.getLineMetrics(0).width;
                if (truncatedWidth > widthToTruncate) {
                    // throw away characters until fits
                    currLoc--;
                    while (truncatedWidth > widthToTruncate && currLoc > 0) {
                        measurementField.text = fullText.substr(0, currLoc) +
                            "..." + rightPart;
                        truncatedWidth = measurementField.getLineMetrics(0).width;
                        currLoc--;
                    }
                    currLoc++;
                } else {
                    // try to add characters one-by-one and
                    // see if it still fits
                    do {
                        currLoc++;
                        measurementField.text = fullText.substr(0, currLoc) +
                            "..." + rightPart;
                        widthWithNextChar = measurementField.getLineMetrics(0).width;
                    } while (widthWithNextChar <= widthToTruncate &&
                        currLoc < len-1);
                    currLoc--;
                }
               
                return fullText.substr(0, Math.max(currLoc,1)) +
                    "..." + rightPart;
            }
            return fullText;
           
        }
    }
}

czwartek, 22 lipca 2010

Flash/Flex debugger and FireFox Flash plugin crash

Someone at Mozilla had this 'great' idea to introduce Flash plugin timeout settings. I'm not really sure what it was supposed to do but it does one thing for sure - crashes my Flash plugin whenever I spend more than 45 second on debugging a single breakpoint in Flex.

Although guys at Mozilla might not think twice about what they are going to implement next they've got a pretty good support.

Here's the solution to this problem: http://support.mozilla.com/pl/kb/The+Adobe+Flash+plugin+has+crashed

środa, 14 lipca 2010

Pass reference to "this" to child object generated by Repeater.

Let's assume that we've got a component named Item. This component has a public property named controller.

The idea is that we want to pass reference to this (usually a parent object of Repeater) to Repeater children.

You could use basic logic and try to do something like this:
<mx:Repeater dataProvider="{dP}">
    <layouts:Item controller="{this}" />
</mx:Repeater>
But unfortunately Flex is not a place where you should use logical solutions. In shown example scope of this is fubar. Thus the necessary workaround which is displayed below. Please note that XXX should be the name of the Class which is represented by this pointer.
[Bindable] private var _this:XXX; 
private function init():void
{
_this = this; 
}
<mx:Repeater dataProvider="{dP}">
    <layouts:Item controller="{_this}" />
</mx:Repeater>

wtorek, 29 czerwca 2010

Error #2038: File I/O Error

If you try to upload a file to the server by using FileReference.upload() method and get aforementioned I/O Error check if the file you're trying to send is larger than 0 bytes.

ActionScript can not upload zero-sized files and handles them by throwing I/O Error.

poniedziałek, 14 czerwca 2010

Flex: Important difference between TileList and Repeater

Let's assume we need to do some custom stuff after sending data, from data provider, to either TileList or Repeater items.

First step is to override commitProperties function in class that will represent item renderers in both cases.

All data passed to the item renderer by its parent object (TileList/Renderer) should be available during the execution of this function (and not only then) in the data variable. The difference is that Renderer does not put that data there automatically. God knows why.

To make it work exactly like TileList, or any other list for that matter, simply preceed all instructions in commitProperties by statement shown on following example:

override protected function commitProperties():void
{
   data = getRepeaterItem();
}

czwartek, 10 czerwca 2010

Flex: Focus issue while chaning cursor with CursorManager

Sometimes when CursorManager is used to swap normal cursor to a custom graphic it won't refresh until mouse pointer is moved to some other element of the application and then moved back.

It happens like that because component gains focus (kind of) on MouseEvent.ROLL_OVER event but making users roll out and over to see a new cursor is not an acceptable solution, unless of course you really don't like those guys, then be my guest and make them suffer.

There are two solutions for this problem:
  • call method UIComponent.setFocus()
    This will work if you don't need focus in any other part of the application, like for an example textfield or on a button
  • componentName.dispatchEvent(new MouseEvent(MouseEvent.ROLL_OVER,true));
    Second solution might not look as sharp as the first one but won't take your focus from other elements and will change cursor the same was as you'd move your mouse out and back from the component you're currently hovering over

środa, 9 czerwca 2010

Flex: component changing height when changing styles

Component <mx:Text> has unfortunate tendency to change its dimensions when its height/width was not specified and styles are being changed.

It's most visible when many components, with <mx:Text> inside them are placed in a container - <mx:Repeater> or even <mx:Vbox> which has a scroll. When those components have different styles assigned to over/normal/selected states they are being redrawn whenever user hovers his mouse over them.

Problem is that during style change process dimensions of <mx:Text> are being reset for a few miliseconds thus changing the dimensions of parent components which results in glitchy display or even resetting the position of scrollbars in parent components.

To fix those problems you have to override its styleName setter - you can use wrapping component for that and do something like this:
override public function set styleName(value:Object):void
{
   super.styleName = value;
   if (tContent && tContent.height >= 18) 
   {
      tContent.height = tContent.height;
   }
}
where
<mx:Text id="tContent" minHeight="18" />
Obviously now you'll want to change the styleName of the wrapping component, not <mx:Text> itself.