2012/09/28

Popup Menu Realized by the New Functions in Flash Player 11.2

In Flash Player 11.2, a significant function has been added: the Popup Menu. Please refer to Flash Player 11.2 Beta Features for details.  The following image is the effect in the Network in TWaver Flex which means that it is unnecessary to screen out the Popup Menu through the js script of html, which method no longer takes effect after the content in flash is full-screening. Because after that, flash is out of the browser and it is of little help to listen to the right button in the browser.
Attentions need to be paid to:
  1. Click here to download Flash Player 11.2 and playerglobal.swc file
  2. Change the name of the downloaded file flashplayer11-2_p3_playerglobal_122011.swc to playerglobal.swc and put it into the corresponding catalogue of SDK(Adobe Flash Builder 4.5/sdks/4.5.1/frameworks/libs/player/11.2)
  3. SDK 4.5 or the edition above is suggested. (The edition below SDK4.0 is not compatible with Flash Player 11.)
  4. Set the edition of Flash Player in the project options as 11.2.
  5. Add the option -swf-version=15 in the project options.
The code:
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx"
               xmlns:twaver="http://www.servasoftware.com/2009/twaver/flex"
               applicationComplete="init()">
    <fx:Script>
        <![CDATA[
            import mx.collections.ArrayCollection;
            import mx.controls.Menu;

            import twaver.*;

            private function init():void {
                var box:ElementBox = new ElementBox();

                var from:Node = new Node();
                from.name = "From";
                from.location = new Point(100, 100);
                box.add(from);

                var to:Node = new Node();
                to.name = "To";
                to.location = new Point(300, 300);
                box.add(to);

                var link:Link = new Link(from, to);
                link.name = "From - To";
                box.add(link);

                network.elementBox = box;
                network.addEventListener('rightClick', handleRighClick);
            }

            private var menu:Menu = null;

            private function handleRighClick(e:MouseEvent):void {
                var element:IElement = network.getElementByMouseEvent(e);
                var myMenuData:ArrayCollection = new ArrayCollection([
                    {label: element == null ? "none" : element.name}
                ]);
                if(menu != null){
                    menu.hide();
                }
                menu = Menu.createMenu(network, myMenuData, false);
                var point:Point = network.globalToLocal(new Point(e.stageX, e.stageY));
                menu.show(point.x, point.y);
            }
        ]]>
    </fx:Script>

    <twaver:Network id="network" width="100%" height="100%" backgroundAlpha="0" backgroundColor="#FF0000"/>
</s:Application>
 

2012/09/27

An Example on the Application of HTML5 WebSocket

After the article An introduction on HTML5 WebSocket, this one will explain the use of WebSocket with an example in which we have combined TWaver HTML5: the background provides the topological data which is sent to every client-side through WebSocket in the form of JSON and after it is received by them it will be shown on the interface by the Network components in TWaver HTML5. The elements can be handled at the client-sides, the results of which are sent to the background through WebSocket, and then the background server will renew and notify all client-sides to refresh their interfaces. After that, there will also be alarms appearing constantly in the background server which will be sent to the client-sides to refresh their interfaces.
General structures
Preparations
You can download jetty and twaver html5 which are necessary in this example.
jetty:http://www.eclipse.org/jetty/

The Directory Structure of Jetty
The following structure is what jetty is decompressed. Run start.jar(java -jar start.jar) to start jetty server(java -jar start.jar)and web items can be put into the catalogue “/webapps”, such as “/webapps/alarm/” in this example.

The Background
Jetty is used at the background whose style is the same as the api of servlet. It can be used according to the use and deployment of Servlet. There are three classes needed to be used in this example.
  • WebSocketServlet-WebSocket Service Class
  • WebSocket–corresponding to a WebSocket client-side
  • WebSocket.Conllection–stands for a WebSocket connection
WebSocketServlet
The full name is org.eclipse.jetty.websocket.WebSocketServlet which is used to provide websocket service. It inherits from HttpServlet and the method public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) has been added. This method is called when the client-side asks for websocket connection for the first time. If it is allowed to establish a connection, then returns a WebSocket instance object. Otherwise returns null.
In this example, we will define an AlarmServlet class which inherits from WebSocketServlet to realize the method "doWebSocketConnect"and returns an "AlarmWebSocket" instance which stands for a client-side.

AlarmServlet
There is a “clients” property in AlarmWebSocket which is used to maintain a client-side (AlarmWebSocket) list. When establishing a connection with the client-side, the corresponding AlarmWebSocket instance of the client-side will be added to the list; when the client-side is closed, it will be deleted from the list.
public class AlarmServlet extends org.eclipse.jetty.websocket.WebSocketServlet {
    private final Set<AlarmWebSocket> clients;//to save the client-side list

    public AlarmServlet() {
        initDatas();//to initialize the data
    }

    @Override
    public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
        return new AlarmWebSocket();
    }
    //...
}
AlarmWebSocket
Let’s see AlarmWebSocket. Here an Inner Class has been defined to realize the three methods of the interface org.eclipse.jetty.websocket.WebSocket.OnTextMessage: onOpen/onMessage/onClose which are called back when establishing connections, receiving messages from client-sides and closing connections respectively. If you have the need to send messages to the client-sides, you can call the method Connection#sendMessage(…) .(The message should all be JSON.) 
      class AlarmWebSocket implements org.eclipse.jetty.websocket.WebSocket.OnTextMessage
   {
       WebSocket.Connection connection;
    @Override
    public void onOpen(Connection connect) {
        this.connection = connect;
        clients.add(this);
        sendMessage(this, "reload", loadDatas());
    }
    @Override
    public void onClose(int code, String message) {
        clients.remove(this);
    }
    @Override
    public void onMessage(String message) {
        Object json = JSON.parse(message);
        if(!(json instanceof Map)){
            return;
        }
        //to analyze the message: the json data in jetty will be analyzed into map objects
        Map map = (Map)json;
        //to renew the data model at the background through the information in the massages
        ...
        //to process the messages to notify other client-sides
        for(AlarmWebSocket client : clients){
            if(this.equals(client)){
                continue;
            }
            sendMessage(client, null, message);
        }
    }
}
private void sendMessage(AlarmWebSocket client, String action, String message){
    try {
        if(message == null || message.isEmpty()){
            message = "\"\"";
        }
        if(action != null){
            message = "{\"action\":\"" + action + "\", \"data\":" + message + "}";
        }
        client.connection.sendMessage(message);
    } catch (IOException e) {
        e.printStackTrace();
    }
}


Background Configuration
The background configuration is the same as serlvet. Set the url as /alarmServer.

<?xml version="1.0" encoding="UTF-8"?>
<web-app
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    metadata-complete="false"
    version="3.0">
    <servlet>
        <servlet-name>alarmServlet</servlet-name>
        <servlet-class>web.AlarmServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>alarmServlet</servlet-name>
        <url-pattern>/alarmServer</url-pattern>
    </servlet-mapping>
</web-app>
 The foreground part
Let’s have a look at the general structure of foreground. Now we can create “websocket” connection to listen to the related events like “onmessage” event. Thus we can receive the massage sent from the background (JSON) and renew it to the interface after analysis.
function init(){
    window.WebSocket = window.WebSocket || window.MozWebSocket;
    if (!window.WebSocket){
        alert("WebSocket not supported by this browser");
        return;
    }
    var websocket = new WebSocket("ws://127.0.0.1:8080/alarm/alarmServer");
    websocket.onopen = onopen;
    websocket.onclose = onclose;
    websocket.onmessage = onmessage;
    ...
}
function onmessage(evt){
    var data = evt.data;
    if(!data){
        return;
    }
    data = stringToJson(data);
    if(!data){
        return;
    }
    ...
}
function jsonToString(json){
    return JSON.stringify(json);
}
function stringToJson(str){
    try{
        str = str.replace(/\'/g, "\"");
        return JSON.parse(str);
    }catch(error){
        console.log(error);
    }
}
Websocket foreground-background process
 
Business Realization
Data Model
There are three business types to be used in this example: node, link and alarm. The background has provided implementation class and defined properties like name, position and line-width. In addition, the export of json data has also been available.
  interface IJSON{
      String toJSON();
  }
  class Data{
      String name;
      public Data(String name){
          this.name = name;
      }
  }
  class Node extends Data implements IJSON{
      public Node(String name, double x, double y){
          super(name);
          this.x = x;
          this.y = y;
      }
      double x, y;
      public String toJSON(){
          return "{\"name\":\"" + name + "\", \"x\":\"" + x + "\",\"y\":\"" + y + "\"}";
      }
  }
  class Link extends Data implements IJSON{
      public Link(String name, String from, String to, int width){
          super(name);
          this.from =from;
          this.to = to;
          this.width = width;
      }
      String from;
      String to;
      int width = 2;
      public String toJSON(){
          return "{\"name\":\"" + name + "\", \"from\":\"" + from + "\", \"to\":\"" + to + "\", \"width\":\"" + width + "\"}";
      }
  }
  class Alarm implements IJSON{
      public Alarm(String elementName, String alarmSeverity){
          this.alarmSeverity = alarmSeverity;
          this.elementName = elementName;
      }
      String alarmSeverity;
      String elementName;
@Override
public String toJSON() {
    return "{\"elementName\": \"" + elementName + "\", \"alarmSeverity\": \"" + alarmSeverity + "\"}";
}
  }
Three lists of data have been maintained in the background to save nodes, links and alarms respectively. Furthermore, it is easy to quickly find nodes since keys are named after node names in elementMap.
Map<String, Data> elementMap = new HashMap<String, AlarmServlet.Data>();
List<Node> nodes = new ArrayList<AlarmServlet.Node>();
List<Link> links = new ArrayList<AlarmServlet.Link>();
List<Alarm> alarms = new ArrayList<AlarmServlet.Alarm>();
To Initialize Data
In servlet construction, we have added some simulated data. When client-side makes connections (AlarmWebSocket#onOpen(Connection connection)), the background will send the information on nodes, links and alarms to the foreground in the form of JSON (sendMessage(this, “reload”, loadDatas());).

public AlarmServlet() {
    initDatas();
    ...
}

public void initDatas() {
    int i = 0;
    double cx = 350, cy = 230, a = 250, b = 180;
    nodes.add(new Node("center", cx, cy));
    double angle = 0, perAngle = 2 * Math.PI/10;
    while(i++ < 10){
        Node node = new Node("node_" + i, cx + a * Math.cos(angle), cy + b * Math.sin(angle));
        elementMap.put(node.name, node);
        nodes.add(node);
        angle += perAngle;
    }
    i = 0;
    while(i++ < 10){
        Link link = new Link("link_" + i, "center", "node_" + i, 1 + random.nextInt(10));
        elementMap.put(link.name, link);
        links.add(link);
    }
}

private String loadDatas(){
    StringBuffer result = new StringBuffer();
    result.append("{\"nodes\":");
    listToJSON(nodes, result);
    result.append(", \"links\":");
    listToJSON(links, result);
    result.append(", \"alarms\":");
    listToJSON(alarms, result);
    result.append("}");
    return result.toString();
}

   class AlarmWebSocket implements org.eclipse.jetty.websocket.WebSocket.OnTextMessage
   {
           ...
    @Override
    public void onOpen(Connection connect) {
        this.connection = connect;
        clients.add(this);
        sendMessage(this, "reload", loadDatas());
    }
           ...
   }
To Show the Initial data in the Foreground
The initial data is sent to the client-side by the method sendMessage(…) from the background and received through the call-back function onmessage by the client-side. In this example, we use components in twaver html5 to show them. The use flow of Twaver components is as usual: first convert the data, that is, to switch JSON data to the element types of TWaver; then fill them to ElementBox; at last relate them to the network components. Here is the code:

public AlarmServlet() {
    initDatas();
    ...
}

public void initDatas() {
    int i = 0;
    double cx = 350, cy = 230, a = 250, b = 180;
    nodes.add(new Node("center", cx, cy));
    double angle = 0, perAngle = 2 * Math.PI/10;
    while(i++ < 10){
        Node node = new Node("node_" + i, cx + a * Math.cos(angle), cy + b * Math.sin(angle));
        elementMap.put(node.name, node);
        nodes.add(node);
        angle += perAngle;
    }
    i = 0;
    while(i++ < 10){
        Link link = new Link("link_" + i, "center", "node_" + i, 1 + random.nextInt(10));
        elementMap.put(link.name, link);
        links.add(link);
    }
}

private String loadDatas(){
    StringBuffer result = new StringBuffer();
    result.append("{\"nodes\":");
    listToJSON(nodes, result);
    result.append(", \"links\":");
    listToJSON(links, result);
    result.append(", \"alarms\":");
    listToJSON(alarms, result);
    result.append("}");
    return result.toString();
}

   class AlarmWebSocket implements org.eclipse.jetty.websocket.WebSocket.OnTextMessage
   {
           ...
    @Override
    public void onOpen(Connection connect) {
        this.connection = connect;
        clients.add(this);
        sendMessage(this, "reload", loadDatas());
    }
           ...
   }
The effect

 
The Background Sends Alarms and the Foreground Instantly Renews
Add the code for sending alarms at the background: Here we have made a timer which generates a random alarm every two seconds or clear all the alarms and send the information to all client-sides.

The background code:
public AlarmServlet() {
    ...
    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            if(random.nextInt(10) == 9){
                alarms.clear();
                sendMessage ("alarm.clear", "");
                return;
            }
            sendMessage("alarm.add", randomAlarm());
        }
    }, 0, 2000);
}
public void sendMessage(String action, String message) {
    for(AlarmWebSocket client : clients){
        sendMessage(client, action, message);
    }
}
private Random random = new Random();
private Data getRandomElement(){
    if(random.nextBoolean()){
        return nodes.get(random.nextInt(nodes.size()));
    }
    return links.get(random.nextInt(links.size()));
}
String[] alarmSeverities = new String[]{"Critical", "Major", "Minor", "Warning", "Indeterminate"};
private String randomAlarm(){
    Alarm alarm = new Alarm(getRandomElement().name, alarmSeverities[random.nextInt(alarmSeverities.length)]);
    alarms.add(alarm);
    return alarm.toJSON();
}
The foreground code:

There should be the corresponding operations when the client-side has received the messages: add the operations to messages like “alarm.clear” and “alarm.add”. In this way alarms can instantly refresh themselves.
function onmessage(evt){
    ...
    if(action == "alarm.clear"){
        box.getAlarmBox().clear();
        return;
    }
    data = data.data;
    if(!data){
        return;
    }
    ...
    if(action == "alarm.add"){
        newAlarm(data)
        return;
    }
    ...
}
Dragging Nodes at a Client-side to Synchronize to Others
At last, add drag synchronization and listen to the element-dragged events. At the end of dragging an element, the position of the node will be sent to the background.

Foreground code:
network.addInteractionListener(function(evt){
    var moveEnd = "MoveEnd";
    if(evt.kind.substr(-moveEnd.length) == moveEnd){
        var nodes = [];
        var selection = box.getSelectionModel().getSelection();
        selection.forEach(function(element){
            if(element instanceof twaver.Node){
                var xy = element.getCenterLocation();
                nodes.push({name: element.getName(), x: xy.x, y: xy.y});
            }
        });
        websocket.send(jsonToString({action: "node.move", data: nodes}));
    }
});
When the background has received the position of the node, the background data (the position of the node) will first be renewed, which will then be sent to other client-side, hence the simultaneous operation among client-sides.

Background code:
   class AlarmWebSocket implements org.eclipse.jetty.websocket.WebSocket.OnTextMessage
   {
           ...
    @Override
    public void onMessage(String message) {
        Object json = JSON.parse(message);
        if(!(json instanceof Map)){
            return;
        }
        Map map = (Map)json;
        Object action = map.get("action");
        Object data = map.get("data");
        if("node.move".equals(action)){
            if(!(data instanceof Object[])){
                return;
            }
            Object[] nodes = (Object[])data;
            for(Object nodeData : nodes){
                if(!(nodeData instanceof Map) || !((Map)nodeData).containsKey("name") || !((Map)nodeData).containsKey("x") || !((Map)nodeData).containsKey("y")){
                    continue;
                }
                String name = ((Map)nodeData).get("name").toString();
                Data element = elementMap.get(name);
                if(!(element instanceof Node)){
                    continue;
                }
                double x = Double.parseDouble(((Map)nodeData).get("x").toString());
                double y = Double.parseDouble(((Map)nodeData).get("y").toString());
                ((Node)element).x = x;
                ((Node)element).y = y;
            }

        }else{
            return;
        }
        for(AlarmWebSocket client : clients){
            if(this.equals(client)){
                continue;
            }
            sendMessage(client, null, message);
        }
    }
}
Structure:

2012/09/21

How to Customize the Tooltip in Tree in TWaver Flex

If you have seen the tooltip in Tree, you must have found that it is too far away from your mouse. What’s more, it is unable to move following the mouse. Worse still, it is quite troublesome to dynamically change the tooltip.
There is no doubt that the customized tooltip is much more flexible.
  1. Set tree.toolTipFunction and close the tooltip in default.

  2. tree.toolTipFunction = function(element:IData):String {
        return null;
    }

  3. Listens to mousemove events to dynamically show, hide and update tooltip.

  4. tree.addEventListener(MouseEvent.MOUSE_MOVE, function(e:MouseEvent):void {
        handleMouseMove(e);
    });

  5. Since even if there is nothing under your mouse there will still be a return value, it needs special handling to judge whether your mouse is on an icon or some text. 

  6. private function showToolTip():Boolean {
        var obj:InteractiveObject = null;
        var mousePoint:Point = new Point(stage.mouseX, stage.mouseY);
        var objects:Array = stage.getObjectsUnderPoint(mousePoint);
        for (var i:int = objects.length-1; i>=0; i--) {
            if (objects[i] is InteractiveObject) {
                obj = objects[i] as InteractiveObject;
                break;
            } else {
                if (objects[i] is Shape && (objects[i] as Shape).parent) {
                    obj = (objects[i] as Shape).parent;
                    break;
                }
            }
        }
        return obj is UIComponent || obj is UITextField;
    }
    The final effect:
     
The complete code:

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
                xmlns:tw="http://www.servasoftware.com/2009/twaver/flex"
                paddingLeft="0" paddingRight="0" paddingTop="0" paddingBottom="0" backgroundColor="#FFFFFF"
                creationComplete="init()">
    <mx:Script>
        <![CDATA[
            import mx.core.IToolTip;
            import mx.core.UIComponent;
            import mx.core.UITextField;
            import mx.managers.ToolTipManager;

            import twaver.DataBox;
            import twaver.IData;
            import twaver.Link;
            import twaver.Node;
            import twaver.controls.TreeData;

            private var _toolTip:IToolTip;
            private var _lastData:IData = null;
            private var _timer:uint = 0;

            private function init():void {
                var box:DataBox = tree.dataBox;
                tree.toolTipFunction = function(element:IData):String {
                    return null;
                };
                tree.addEventListener(MouseEvent.MOUSE_MOVE, function(e:MouseEvent):void {
                    handleMouseMove(e);
                });
                tree.callLater2(function():void {
                    tree.expandAll();
                });

                var from:Node = new Node();
                from.name = "From";
                from.toolTip = "From";
                from.centerLocation = new Point(100, 100);
                box.add(from);
                var to:Node = new Node();
                to.name = "To";
                to.toolTip = "To";
                to.centerLocation = new Point(300, 400);
                box.add(to);
                var link:Link = new Link(from, to);
                link.name = "Link";
                link.toolTip = "Link";
                box.add(link);
            }

            private function handleMouseMove(e:MouseEvent):void {
                var treeData:TreeData = tree.getTreeDataByMouseEvent(e);
                var data:IData = (showToolTip() && treeData && treeData.data) ? treeData.data : null;
                if(data != null){
                    var x:Number = tree.mouseX + 10, y:Number = tree.mouseY + 10;
                    if(data != _lastData){
                        if(_toolTip != null){
                            ToolTipManager.destroyToolTip(_toolTip);
                        }
                        _toolTip = ToolTipManager.createToolTip(data.toolTip, x, y);
                        if(_timer){
                            clearTimeout(_timer);
                        }
                        _timer = setTimeout(function():void {
                            _timer = 0;
                            if(_toolTip){
                                _toolTip.text = "test\n" + _toolTip.text;
                            }
                        }, 1000);
                    }else{
                        _toolTip.x = x;
                        _toolTip.y = y;
                    }

                }else{
                    if(_toolTip != null){
                        ToolTipManager.destroyToolTip(_toolTip);
                    }
                    _toolTip = null;
                }
                _lastData = data;
            }

            private function showToolTip():Boolean {
                var obj:InteractiveObject = null;
                var mousePoint:Point = new Point(stage.mouseX, stage.mouseY);
                var objects:Array = stage.getObjectsUnderPoint(mousePoint);
                for (var i:int = objects.length-1; i>=0; i--) {
                    if (objects[i] is InteractiveObject) {
                        obj = objects[i] as InteractiveObject;
                        break;
                    } else {
                        if (objects[i] is Shape && (objects[i] as Shape).parent) {
                            obj = (objects[i] as Shape).parent;
                            break;
                        }
                    }
                }
                return obj is UIComponent || obj is UITextField;
            }
        ]]>
    </mx:Script>
    <tw:Tree id="tree" width="30%" height="100%"/>
</mx:Application>
 

2012/09/13

To Customize a Tooltip with an Icon in Flex

Customization is undoubtedly the biggest characteristic of TWaver: node, link, attachment, even tooltip can be customized. It shows some more powerful and more complex functions. Now we will come to an example of customizing Tooltip.
Now let’s analyze the function in detail. Since that when mouse is over the target tooltip shows and does not when mouse is out, we can define a tooltip component to listen to the mouse-move events in the network: if there is an element under the mouse, then the tooltip component will show and the location of the tooltip will be dynamically calculated; otherwise it will be hidden.
this.network.addEventListener(MouseEvent.MOUSE_MOVE, function(e:MouseEvent):void
updateToolTip(e);
});

private function updateToolTip(e:MouseEvent):void {
    var element:IElement = network.getElementByMouseEvent(e, true, 5);
    if(lastElement == element){
        return;
    }
    lastElement = element;
    if(element is Link){
        var point:Point = network.getLogicalPoint(e);
        customTooltip.x = point.x - customTooltip.measuredWidth / 2;
        customTooltip.y = point.y - customTooltip.measuredHeight - 10;
        customTooltip.setText(element.getClient('message'));
        customTooltip.visible = true;
    }else{
        customTooltip.visible = false;
    }
}
Let’s go further to how to realize the tooltip component. First we need to define a class—tooltip, which inherits from canvas. In this way, we can add tooltip directly to network.topCanvas.
public class CustomToolTip extends Canvas {}
Since the interactions and the scroll bars are not needed in the tooltip component, they can be disabled in it.
public function CustomToolTip() {
    this.mouseEnabled = false;
    this.mouseChildren = false;
    this.horizontalScrollPolicy = ScrollPolicy.OFF;
    this.verticalScrollPolicy = ScrollPolicy.OFF;
    this.init();
}
The key point is how to draw the customized tooltip. We need to add the icon and the texts into the tooltip component and at the same time calculate their location in it.

var messageImage:Image = new Image();
messageImage.source =  new messageIcon();
messageImage.x = _hmargin;
messageImage.y = _vmargin;
this.addChild(messageImage);

_component = new UIComponent();
this.addChild(_component);

_message = new TextField();
_message.autoSize = TextFieldAutoSize.LEFT;
_message.x = _hmargin + _iconWidth + _hgap;
_message.y = _vmargin;
_component.addChild(_message);
Next, we need to calculate the width and the height of the tooltip shown in the network, which is dynamically calculated according to the size of texts set in it. Therefore we have rewritten the method measure to calculate the actual width and height of the tooltip.
override protected function measure():void {
    super.measure();
    this.measuredWidth = _hmargin * 2 + _iconWidth + _hgap + _message.width;
    this.measuredHeight = _vmargin + _iconHeight + _vgap + _arrowHeight;
}
Now we are going to make a figure like the shape of tooltip. We all know that tooltip is a rectangle and to beautify it we can make a rectangle with rounded corners, and do not forget that there should be a small triangle under it. The shape can be made by the drawing pen.
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
    super.updateDisplayList(unscaledWidth, unscaledHeight);
    var g:Graphics = this.graphics;
    g.clear();
    var lineWidth:Number = 1;
    g.lineStyle(lineWidth, 0, 0.5, true, "normal", CapsStyle.ROUND, JointStyle.ROUND);

    var _width:Number = unscaledWidth, _height:Number = unscaledHeight;
    Utils.beginFill(g, 0xFFFFFF, 1, 0, 0, _width, _height, Consts.GRADIENT_LINEAR_EAST, 0xCCCCCC, 1);
    g.drawRoundRect(lineWidth, lineWidth, _width - lineWidth * 2, _height - lineWidth * 2 - _arrowHeight, 10, 10);
    g.moveTo(_arrowStart, _height - lineWidth - _arrowHeight);
    g.lineTo(_arrowStart, _height);
    g.lineTo(_arrowStart + _arrowWidth, _height - lineWidth - _arrowHeight);
    g.endFill();
    g.lineStyle(1, 0xFFFFFF);
    g.moveTo(_arrowStart, _height - lineWidth - _arrowHeight);
    g.lineTo(_arrowStart + _arrowWidth, _height - lineWidth - _arrowHeight);
}
When setting the text in the tooltip, the methods invalidateSize and invalidateDisplayList are needed to be called to recalculate the width and the height of the tooltip by the program.
public function setText(message:String):void {
            _message.htmlText = "<font color='#000000'>" + message + "</font>";
            this.invalidateSize();
            this.invalidateDisplayList();
        }
When Tooltip has been customized, it is necessary to bind up the element and the content in the customized tooltip.
link.setClient('message', '3333M');
customTooltip.setText(element.getClient('message'));

How to Show the Horizontal Scroll Bar on the Tree in Flex

In our official forum, many people have asked that why there is still no horizontal scroll bar when they have already set “horizontalScrollPolicy” of “twaver.controls.Tree” as ScrollPolicy.AUTO. This is that the value of maxHorizontalScrollPosition is not calculated for the sake of high performance in Adobe. In this article, we have provided the solution, please see the following effect:
Inherit from the original Tree and get the new class AutoSizeTree:
package {
    import flash.events.Event;

    import mx.core.ScrollPolicy;
    import mx.core.mx_internal;

    import twaver.DataBox;
    import twaver.controls.Tree;

    public class AutoSizeTree extends Tree {
        public function AutoSizeTree(dataBox:DataBox=null) {
            super(dataBox);
            horizontalScrollPolicy = ScrollPolicy.AUTO;
        }

        override public function get maxHorizontalScrollPosition():Number {
            if (isNaN(mx_internal::_maxHorizontalScrollPosition))
                return 0;

            return mx_internal::_maxHorizontalScrollPosition;
        }

        override public function set maxHorizontalScrollPosition(value:Number):void {
            mx_internal::_maxHorizontalScrollPosition = value;
            dispatchEvent(new Event("maxHorizontalScrollPositionChanged"));

            scrollAreaChanged = true;
            invalidateDisplayList();
        }

        override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
            var diffWidth:Number = measureWidthOfItems(0,0) - (unscaledWidth - viewMetrics.left - viewMetrics.right);

            if (diffWidth <= 0)
                maxHorizontalScrollPosition = NaN;
            else
                maxHorizontalScrollPosition = diffWidth;

            super.updateDisplayList(unscaledWidth, unscaledHeight);
        }
    }
}
Test Code:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
                xmlns:tw="http://www.servasoftware.com/2009/twaver/flex"
                applicationComplete="init()" xmlns:local="*">
    <mx:Script>
        <![CDATA[
            import twaver.*;

            private function init():void {
                var box:ElementBox = new ElementBox();

                var group:Group = new Group();
                group.name = "Group";
                box.add(group);

                for(var i:int=0; i<20; i++){
                    var node:Node = new Node();
                    node.name = "Node with long long long long long long name " + Utils.randomInt(10000000);
                    group.addChild(node);
                    box.add(node);
                }

                tree.dataBox = box;
                tree.callLater2(function():void {
                    tree.expandAll();
                });
            }
        ]]>
    </mx:Script>
    <local:AutoSizeTree id="tree" width="300" height="100%"/>
</mx:Application>
If you do not have a large amount of data, the performance will be, of course, very high. Why don’t have a try?