2012/07/27

Add the Function “AutoComplete” in JTextField

Nowadays, UE (user experience) has been increasingly emphasized. Even a simple textfield has become more and more user-friendly. For example, when we enter texts into a input box through google, there will appear a Combobox which lists the most frequently words searched by people and at the same time similar to the currently input words. Of course, this helps us to quickly input our text; the same as Baidu. It is just abound with examples, such as the general mail client.

Then, is it easy for Swing’s textbox to realize this? Examples are also available on the Net, but either too simple-functioned or too wordy-coded. In addition, some commercial Swing components are not free-charged. This article involves the application of the free ERP software development in 2BizBox and manages to achieve the effects with a very easy and effective way.
At first, let’s see it closely: it is a textbox, not a ComboBox, from appearance and nature. Therefore, we would not like to make it in to a ComboBox, that is, we don’t feel like inheriting from JComboBox. Next, ComboBox is dynamic and completely nonsynchronous with the input field because it is just a suggestion, unable to interfere with the texts input. Finally, the appearance and the act of the ComboBox is undoubtedly that of the ComboBox of JComboBox. So, “the JTextField which is able to complete automatically” should be a combination of the ComboBox of both JTextField and JComboBox.
After the analysis above, we can make sure that it is actually a JTextField, but also combined with a JComboBox’s ComboBox. Then in this case it on earth inherits from which one? From JTextField?
In fact, inheriting from others is not the best way. Why? It means that only by inheriting from your class can others use this function. If there are already more than 10 thousand interfaces written in your project and you would like to add the intelligent suggesting functions into some of the textboxes, will you change all of these codes in order to let them inherit your class? There is no doubt that that is an awful idea. For this reason, it’s not good for those people who just learned OO to inherit at every move. Is it not a better idea that we just offer a method Util to make a little change on the already existing JTextField instances, thus having the intelligent suggesting functions?
In order to combine the two components JTextField and JComboBox, here we use a very “special”method which is definitely out of your imagination, that is, put a JComboBox into JTextField and make that invisible. Let’s see the code:

JTextField txtInput = new JTextField();
JComboBox cbInput = new JComboBox();
txtInput.setLayout(new BorderLayout());
txtInput.add(cbInput, BorderLayout.SOUTH);

What? Set JTextField as a layout and add a JComboBox to put it in south? I believe that you have never heard of that. It is so peculiar. But it looks perfect once you set the height of JComboBox as 0.

JComboBox cbInput = new JComboBox(model) {
    public Dimension getPreferredSize() {
        return new Dimension(super.getPreferredSize().width, 0);
    }
};

Although you can’t see Combo, it does exist in the textbox and is at the south of it. Our idea is: when texts are typed into the textbox, it will judge whether there are something conform to the requirements in the ComboBox. If there is, then the ComboBox will pop out immediately or it will disappear.
It is not difficult to keep an eye on the textbox: add listener to its document. Here we have used the rules as “case-insensitive” and “the items having the same opening as the string input”to filter. All the alternative strings are put in a single array, and those which conform to the conditions will be filtered out dynamically. Then they will be added into JComboBox and pop up the ComboBox.

txtInput.getDocument().addDocumentListener(new DocumentListener() {
    public void insertUpdate(DocumentEvent e) {
        updateList();
    }

    public void removeUpdate(DocumentEvent e) {
        updateList();
    }

    public void changedUpdate(DocumentEvent e) {
        updateList();
    }

    private void updateList() {
        setAdjusting(cbInput, true);
        model.removeAllElements();
        String input = txtInput.getText();
        if (!input.isEmpty()) {
            for (String item : items) {
                if (item.toLowerCase().startsWith(input.toLowerCase())) {
                    model.addElement(item);
                }
            }
        }
        cbInput.setPopupVisible(model.getSize() > 0);
        setAdjusting(cbInput, false);
    }
});

In addition, for much more convenience, we have added several shortcut keys: when ESC is input, the ComboBox will be closed automatically; when inputting a carriage return or a space, the first string which meets the requirement will be input in the textbox directly:

txtInput.addKeyListener(new KeyAdapter() {

    @Override
    public void keyPressed(KeyEvent e) {
        setAdjusting(cbInput, true);
        if (e.getKeyCode() == KeyEvent.VK_SPACE) {
            if (cbInput.isPopupVisible()) {
                e.setKeyCode(KeyEvent.VK_ENTER);
            }
        }
        if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
            e.setSource(cbInput);
            cbInput.dispatchEvent(e);
            if (e.getKeyCode() == KeyEvent.VK_ENTER) {
                txtInput.setText(cbInput.getSelectedItem().toString());
                cbInput.setPopupVisible(false);
            }
        }
        if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
            cbInput.setPopupVisible(false);
        }
        setAdjusting(cbInput, false);
    }
});

There is another important method needed to be explained: when popping up the popup list, we want that the arrow on keyboard can select the items in the list both upward and downward and that at the same time the current cursor and the focus still stay in the textbox. This seems to be very difficult. Please look at how we make it: when we keep an eye on the textbox and find out that the items in the list are to be input by the up and down arrows, we change source of the current keyboard events to JComboBox and then distribute it to JComboBox. That is to say, originally, the source of the event is the textbox, but now we have intercepted the postman, changed the receiver and then continued to give it back to him. In this way, we have grafted one twig on another.

if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
    e.setSource(cbInput);
    cbInput.dispatchEvent(e);
    if (e.getKeyCode() == KeyEvent.VK_ENTER) {
        txtInput.setText(cbInput.getSelectedItem().toString());
        cbInput.setPopupVisible(false);
    }
}

At last, to see the effects, we need to put some data in the ComboBox. Then what to put? We choose “all countries” in Java to avoid troubleness.

Locale[] locales = Locale.getAvailableLocales();
for (int i = 0; i < locales.length; i++) {
    String item = locales[i].getDisplayName();
    items.add(item);
}

Now let’s see the effect. Good. It well lives up to our expectations.
The complete code is listed as follow:

import java.awt.*;
import java.awt.event.*;
import java.util.*;

import javax.swing.*;
import javax.swing.event.*;

import twaver.*;

public class Test {

    public static void main(String[] args) throws Exception {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        JFrame frame = new JFrame();
        frame.setTitle("Auto Completion Test");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setBounds(200, 200, 500, 400);

        ArrayList<String> items = new ArrayList<String>();
        Locale[] locales = Locale.getAvailableLocales();
        for (int i = 0; i < locales.length; i++) {
            String item = locales[i].getDisplayName();
            items.add(item);
        }
        JTextField txtInput = new JTextField();
        setupAutoComplete(txtInput, items);
        txtInput.setColumns(30);
        frame.getContentPane().setLayout(new FlowLayout());
        frame.getContentPane().add(txtInput, BorderLayout.NORTH);
        frame.setVisible(true);
    }

    private static boolean isAdjusting(JComboBox cbInput) {
        if (cbInput.getClientProperty("is_adjusting") instanceof Boolean) {
            return (Boolean) cbInput.getClientProperty("is_adjusting");
        }
        return false;
    }

    private static void setAdjusting(JComboBox cbInput, boolean adjusting) {
        cbInput.putClientProperty("is_adjusting", adjusting);
    }

    public static void setupAutoComplete(final JTextField txtInput, final ArrayList<String> items) {
        final DefaultComboBoxModel model = new DefaultComboBoxModel();
        final JComboBox cbInput = new JComboBox(model) {
            public Dimension getPreferredSize() {
                return new Dimension(super.getPreferredSize().width, 0);
            }
        };
        setAdjusting(cbInput, false);
        for (String item : items) {
            model.addElement(item);
        }
        cbInput.setSelectedItem(null);
        cbInput.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!isAdjusting(cbInput)) {
                    if (cbInput.getSelectedItem() != null) {
                        txtInput.setText(cbInput.getSelectedItem().toString());
                    }
                }
            }
        });

        txtInput.addKeyListener(new KeyAdapter() {

            @Override
            public void keyPressed(KeyEvent e) {
                setAdjusting(cbInput, true);
                if (e.getKeyCode() == KeyEvent.VK_SPACE) {
                    if (cbInput.isPopupVisible()) {
                        e.setKeyCode(KeyEvent.VK_ENTER);
                    }
                }
                if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
                    e.setSource(cbInput);
                    cbInput.dispatchEvent(e);
                    if (e.getKeyCode() == KeyEvent.VK_ENTER) {
                        txtInput.setText(cbInput.getSelectedItem().toString());
                        cbInput.setPopupVisible(false);
                    }
                }
                if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                    cbInput.setPopupVisible(false);
                }
                setAdjusting(cbInput, false);
            }
        });
        txtInput.getDocument().addDocumentListener(new DocumentListener() {
            public void insertUpdate(DocumentEvent e) {
                updateList();
            }

            public void removeUpdate(DocumentEvent e) {
                updateList();
            }

            public void changedUpdate(DocumentEvent e) {
                updateList();
            }

            private void updateList() {
                setAdjusting(cbInput, true);
                model.removeAllElements();
                String input = txtInput.getText();
                if (!input.isEmpty()) {
                    for (String item : items) {
                        if (item.toLowerCase().startsWith(input.toLowerCase())) {
                            model.addElement(item);
                        }
                    }
                }
                cbInput.setPopupVisible(model.getSize() > 0);
                setAdjusting(cbInput, false);
            }
        });
        txtInput.setLayout(new BorderLayout());
        txtInput.add(cbInput, BorderLayout.SOUTH);
    }
}

2012/07/26

To Realize the Vertical-text Layout in Twaver Flex

Recently, some clients have asked how to show the texts of the labels in Network in vertical direction. The first thing I think of is that we can insert a carriage return between every two characters which can be made by Network#labelFunction.

network.labelFunction = function (element:IElement):String {
    var name:String = element.name;
    if(element.getClient('vertical')) {
        var result:String = '';
        for(var i:int=0,n:int=name.length; i<n; i++) {
            result += name.charAt(i) + '\n';
        }
        result = result.substr(0, result.length-1);
        return result;
    } else {
        return name;
    }
};

Let’s test it with some codes:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
                xmlns:twaver="http://www.servasoftware.com/2009/twaver/flex"
                layout="absolute" width="100%" height="100%"
                creationComplete="init()" backgroundColor="#FFFFFF" >
    <mx:Script>
        <![CDATA[
            import twaver.Consts;
            import twaver.ElementBox;
            import twaver.IElement;
            import twaver.Node;
            import twaver.Styles;

            private var box:ElementBox = new ElementBox();

            private function init():void {
                network.labelFunction = function (element:IElement):String {
                    var name:String = element.name;
                    if(element.getClient('vertical')) {
                        var result:String = '';
                        for(var i:int=0,n:int=name.length; i<n; i++) {
                            result += name.charAt(i) + '\n';
                        }
                        result = result.substr(0, result.length-1);
                        return result;
                    } else {
                        return name;
                    }
                };

                var node1:Node = new Node();
                node1.location = new Point(100, 100);
                node1.setStyle(Styles.LABEL_POSITION, Consts.POSITION_LEFT_LEFT);
                node1.setClient('vertical', true);
                node1.name = '竖向文字Vertical Text';
                box.add(node1);

                network.elementBox = box;
            }
        ]]>
    </mx:Script>
    <twaver:Network id="network" width="100%" height="100%" />
</mx:Application>

You can see the effects:

The method mentioned above can easily show the texts vertically while the effect is not agreeable, because when Chinese characters and English words are mingled together the characters of English words are separated one by one. Is there a better solution? There must be, for we can easily show words from top to bottom by the means of Flash Text Engine (FTE) and Text Layout Framework (TLF) in Flex.
Let’s first see an example: set blockProgression of TextLayoutFormat as BlockProgression.RL.

package {
    import flash.display.Sprite;

    import flashx.textLayout.container.ContainerController;
    import flashx.textLayout.conversion.TextConverter;
    import flashx.textLayout.elements.TextFlow;
    import flashx.textLayout.formats.BlockProgression;
    import flashx.textLayout.formats.TextLayoutFormat;

    public class StaticHelloWorld extends Sprite {
        public function StaticHelloWorld() {
            var textLayoutFormat:TextLayoutFormat = new TextLayoutFormat();
            textLayoutFormat.lineHeight = 30;
            textLayoutFormat.locale = 'zh';
            textLayoutFormat.blockProgression = BlockProgression.RL;

            var text:String = "测试竖向文字,再看看English如何?";
            var textFlow:TextFlow = TextConverter.importToFlow(text, TextConverter.PLAIN_TEXT_FORMAT);
            textFlow.hostFormat = textLayoutFormat;
            textFlow.flowComposer.addController(new ContainerController(this, 25, 200));
            textFlow.flowComposer.updateAllControllers();
        }
    }
}

See the effects as follows:
Now the effects are better as the English characters are not separated one by one. Then let’s define an Attachment:

package {
    import flash.display.Sprite;
    import flash.text.engine.FontPosture;
    import flash.text.engine.FontWeight;

    import flashx.textLayout.container.ContainerController;
    import flashx.textLayout.elements.ParagraphElement;
    import flashx.textLayout.elements.SpanElement;
    import flashx.textLayout.elements.TextFlow;
    import flashx.textLayout.formats.BlockProgression;
    import flashx.textLayout.formats.TextDecoration;
    import flashx.textLayout.formats.TextLayoutFormat;

    import twaver.Styles;
    import twaver.network.ui.BasicAttachment;
    import twaver.network.ui.ElementUI;

    public class FTELabelAttachment extends BasicAttachment {

        private var textLayoutFormat:TextLayoutFormat = new TextLayoutFormat();

        public function FTELabelAttachment(elementUI:ElementUI, showInAttachmentCanvas:Boolean=false) {
            super(elementUI, showInAttachmentCanvas);

            this.textLayoutFormat.locale = 'zh';
            this.textLayoutFormat.blockProgression = BlockProgression.RL;
        }

        override public function updateProperties():void {
            super.updateProperties();

            this.textLayoutFormat.fontFamily = element.getStyle(Styles.LABEL_FONT);
            this.textLayoutFormat.color = element.getStyle(Styles.LABEL_COLOR);
            this.textLayoutFormat.fontSize = element.getStyle(Styles.LABEL_SIZE);
            this.textLayoutFormat.fontStyle = element.getStyle(Styles.LABEL_ITALIC) ? FontPosture.ITALIC : FontPosture.NORMAL;
            this.textLayoutFormat.fontWeight = element.getStyle(Styles.LABEL_BOLD) ? FontWeight.BOLD : FontWeight.NORMAL;
            this.textLayoutFormat.textDecoration = element.getStyle(Styles.LABEL_UNDERLINE ? TextDecoration.UNDERLINE : TextDecoration.NONE);

            var textFlow:TextFlow = new TextFlow();
            textFlow.hostFormat = this.textLayoutFormat;
            var p:ParagraphElement = new ParagraphElement();
            textFlow.addChild(p);
            var span:SpanElement = new SpanElement();
            span.text = network.getLabel(element);
            p.addChild(span);

            var fteLabel:Sprite = new Sprite();
            this.content = fteLabel;
            var containerController:ContainerController = new ContainerController(fteLabel, this.textLayoutFormat.fontSize, 1000);
            textFlow.flowComposer.addController(containerController);
            textFlow.flowComposer.updateAllControllers();
        }

        override public function get position():String {
            return element.getStyle(Styles.LABEL_POSITION);
        }

        override public function get xOffset():Number {
            return element.getStyle(Styles.LABEL_XOFFSET);
        }

        override public function get yOffset():Number {
            return element.getStyle(Styles.LABEL_YOFFSET);
        }

        override public function get padding():Number {
            return element.getStyle(Styles.LABEL_PADDING);
        }

        override public function get paddingLeft():Number {
            return element.getStyle(Styles.LABEL_PADDING_LEFT);
        }

        override public function get paddingRight():Number {
            return element.getStyle(Styles.LABEL_PADDING_RIGHT);
        }

        override public function get paddingTop():Number {
            return element.getStyle(Styles.LABEL_PADDING_TOP);
        }

        override public function get paddingBottom():Number {
            return element.getStyle(Styles.LABEL_PADDING_BOTTOM);
        }

        override public function get cornerRadius():Number {
            return element.getStyle(Styles.LABEL_CORNER_RADIUS);
        }

        override public function get pointerLength():Number {
            return element.getStyle(Styles.LABEL_POINTER_LENGTH);
        }

        override public function get pointerWidth():Number {
            return element.getStyle(Styles.LABEL_POINTER_WIDTH);
        }

        override public function get direction():String {
            return element.getStyle(Styles.LABEL_DIRECTION);
        }

        override public function get fill():Boolean {
            return element.getStyle(Styles.LABEL_FILL);
        }

        override public function get fillColor():Number {
            return element.getStyle(Styles.LABEL_FILL_COLOR);
        }

        override public function get fillAlpha():Number {
            return element.getStyle(Styles.LABEL_FILL_ALPHA);
        }

        override public function get gradient():String {
            return element.getStyle(Styles.LABEL_GRADIENT);
        }

        override public function get gradientColor():Number {
            return element.getStyle(Styles.LABEL_GRADIENT_COLOR);
        }

        override public function get gradientAlpha():Number {
            return element.getStyle(Styles.LABEL_GRADIENT_ALPHA);
        }

        override public function get contentXScale():Number {
            return element.getStyle(Styles.LABEL_CONTENT_XSCALE);
        }

        override public function get contentYScale():Number {
            return element.getStyle(Styles.LABEL_CONTENT_YSCALE);
        }

        override public function get outlineWidth():Number {
            return element.getStyle(Styles.LABEL_OUTLINE_WIDTH);
        }

        override public function get outlineColor():Number {
            return element.getStyle(Styles.LABEL_OUTLINE_COLOR);
        }

        override public function get outlineAlpha():Number {
            return element.getStyle(Styles.LABEL_OUTLINE_ALPHA);
        }
    }
}

Then, define Node and NodeUI and use this Attachment to replace LabelAttachment contained in TWaver.
Self-define Node:

package {
    import twaver.Node;

    public class FTELabelNode extends Node {
        public function FTELabelNode(id:Object=null) {
            super(id);
        }

        public override function get elementUIClass():Class {
            return FTELabelNodeUI;
        }
    }
}

Self-define NodeUI:

package {
    import twaver.Node;
    import twaver.network.Network;
    import twaver.network.ui.NodeUI;

    public class FTELabelNodeUI extends NodeUI {

        private var _labelAttachment:FTELabelAttachment = null;

        public function FTELabelNodeUI(network:Network, node:Node) {
            super(network, node);
        }

        override protected function checkLabelAttachment():void{
            var label:String = this.network.getLabel(element);
            if(label != null && label != ""){
                if(this._labelAttachment == null){
                    this._labelAttachment = new FTELabelAttachment(this, false);
                    this.addAttachment(this._labelAttachment);
                }
            }else{
                if(this._labelAttachment != null){
                    this.removeAttachment(this._labelAttachment);
                    this._labelAttachment = null;
                }
            }
        }
    }
}

Finally, let’s see the effect of another example:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
                xmlns:twaver="http://www.servasoftware.com/2009/twaver/flex"
                layout="absolute" width="100%" height="100%"
                creationComplete="init()" backgroundColor="#FFFFFF" >
    <mx:Script>
        <![CDATA[
            import twaver.Consts;
            import twaver.ElementBox;
            import twaver.IElement;
            import twaver.Node;
            import twaver.Styles;

            private var box:ElementBox = new ElementBox();

            private function init():void {
                network.labelFunction = function (element:IElement):String {
                    var name:String = element.name;
                    if(element.getClient('vertical')) {
                        var result:String = '';
                        for(var i:int=0,n:int=name.length; i<n; i++) {
                            result += name.charAt(i) + '\n';
                        }
                        result = result.substr(0, result.length-1);
                        return result;
                    } else {
                        return name;
                    }
                };

                var node1:Node = new Node();
                node1.location = new Point(100, 100);
                node1.setStyle(Styles.LABEL_POSITION, Consts.POSITION_LEFT_LEFT);
                node1.setClient('vertical', true);
                node1.name = '竖向文字Vertical Text';
                box.add(node1);

                var node2:Node = new FTELabelNode();
                node2.location = new Point(300, 100);
                node2.setStyle(Styles.LABEL_POSITION, Consts.POSITION_LEFT_LEFT);
                node2.name = '竖向文字Vertical Text';
                box.add(node2);

                network.elementBox = box;
            }

            private function changeFontSize():void {
                box.forEach(function (element:IElement):void {
                    element.setStyle(Styles.LABEL_SIZE, element.getStyle(Styles.LABEL_SIZE) + 2);
                });
            }
        ]]>
    </mx:Script>
    <mx:VBox width="100%" height="100%">
        <mx:HBox width="100%" height="20">
            <mx:Button label="Change Font Size" click="changeFontSize()"/>
        </mx:HBox>
        <twaver:Network id="network" width="100%" height="100%" />
    </mx:VBox>
</mx:Application>

Yeah. This is the very effect I want:
For more information on FTE and TLF, please refer to the official document of Adobe:
TextLayoutFormat
TextFlow
Textlayout

2012/07/16

The Presentation of the Multi-layer Structure of Network with 3D

The discussion on the application of 3D scenario to modern network management never stops. For example, how many application scenarios and the value of the application there are? What are they? How many benefits can it bring to product development and system use? Now I’m going to introduce an application scenario to you and hope that through this opportunity more requirements on applications will be elicited, thus enriching people's application system.
Recently a requirement shared by many people appears. They want multi-layer structure of network to be shown in the same page. The advantages of it are:  the logical and physical structures of a network are distinctly and clearly shown in the same page. At the same time, the interaction between different layers, like changing the linking relationship between the inner and the outer nodes of a subnetwork, will not involve the complicated operations as drilling down or up, or collapsing or expanding groups.
TWaver has already provided the example code for the page mentioned above with 2D whose result is already agreeable. However, with the development of technology, clients have made further requirements and they have pointed out some disadvantages of the original way: lacking three-dimension and being no three-dimensional interactions in the scenario. At the other hand, if there are a lot of layers and nodes are dispersedly spread, it is very troublesome to make such graph.
In the interface, we generally show the hierarchical relationship of nodes with their different height. As is shown in the following image, the node at a high position and the one at a low position belong to different layers and the nodes of almost the same height are in the same layer. 3D objects all have a Z position to show their height. In this way, why don’t we show the hierarchical relationship of nodes through the difference in Z positions?
First, let’s find the data in the subnetwork. We consider the data in the same subnetwork as being in the same layer, namely, they share the same Z position.

private function createGroup1():void{
    var box:ElementBox = network.elementBox;
    var bus:Node = NodeUtils.buildBus(new Vector3D(startx,ly,b1z),
                                                        new Vector3D(b1xr,ly,b1z),12,0xa0a0a0);
    box.add(bus);
    box.add(NodeUtils.buildNodeWithCommon(gpg(0,0,0),
        serverSize,s1,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(2,0,0),
        serverSize,s1,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(4,0,0),
        serverSize,s3,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(0,0,0),
        serverSize,s2,null,null,null,"s"));
    box.add(NodeUtils.buildLink([gpg(0,ly,0),gpg(0,ly,8)],3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(2,ly,0),gpg(2,ly,8)],3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(4,ly,0),gpg(4,ly,8)],3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(2,ly,8),gpg(2,ly,10),gpg(8,ly,10)],
        8,0x20AA20));

    box.add(NodeUtils.buildNodeWithCommon(gpg(8,0,10),
        srSize,csr,null,null,null,"s"));

    box.add(NodeUtils.buildBus(gpg(4,ly,14),gpg(8,ly,14),
        12,0xa0a0a0));
    box.add(NodeUtils.buildNodeWithCommon(gpg(5,0,17),
        serverSize,s1,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(7,sy,17),
        smallSize,s1,null,null,null,"s"));
    box.add(NodeUtils.buildLink([gpg(5,ly,17),gpg(5,ly,14)],
        3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(7,ly,17),gpg(7,ly,14)],
        3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(6,ly,14),gpg(6,ly,12),gpg(8,ly,12),gpg(8,ly,10)],
        8,0x20AA20));
    box.add(NodeUtils.buildBus(gpg(8,ly,10),gpg(10,ly,10),10,0x00a0a0));
    box.add(NodeUtils.buildBus(gpg(10,ly,8),gpg(10,ly,12),12,0xa0a0a0));
    box.add(NodeUtils.buildLink([gpg(10,ly,9),gpg(13,ly,9)],3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(10,ly,11),gpg(13,ly,11)],3,0x00AAA0));
    box.add(NodeUtils.buildNodeWithCommon(gpg(13,sy,9),
        smallSize,s2,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(13,sy,11),
        smallSize,s2,null,null,null,"s"));

    box.add(NodeUtils.buildLink([gpg(13,ly,9),gpg(12,ly,5)],3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(13,ly,9),gpg(16,ly,9)],3,0x00AAA0));
    box.add(NodeUtils.buildNodeWithCommon(gpg(12,0,5),
        serverSize,s3,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(16,0,9),
         serverSize,s1,null,null,null,"s"));

    box.add(NodeUtils.buildBillboard(gpg(22,ly,9),"cloud"));
    box.add(NodeUtils.buildLink([gpg(16,ly,9),gpg(22,ly,9)],3,0x00AAA0));

    box.add(NodeUtils.buildLink([gpg(13,ly,11),gpg(18,ly,11)],3,0xAA00A0));
    box.add(NodeUtils.buildLink([gpg(13,ly,11),gpg(17,ly,17)],3,0xAA00A0));
    box.add(NodeUtils.buildLink([gpg(13,ly,11),gpg(15,ly,19)],3,0xAA00A0));
    box.add(NodeUtils.buildLink([gpg(13,ly,11),gpg(10,ly,19)],3,0xAA00A0));
    box.add(NodeUtils.buildNodeWithCommon(gpg(18,sy,11),
        smallSize,s3,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(17,sy,17),
        smallSize,s1,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(15,sy,19),
        smallSize,s2,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(10,sy,19),
        smallSize,s2,null,null,null,"s"));

    var plane:Node = NodeUtils.buildPlane(new Vector3D(0,-42,0),
        new Vector3D(1000,1000,1000),0xE0E0E0,"floor");
    box.add(plane);
    plane.setStyle(Style3D.ZORDERING_LAYER,-100);
    box.add(NodeUtils.createText(new Vector3D(0,-35,-400),
        "Group A",0xFFA0A0));

}

Second, let’s create the data in another subnetwork. To differentiate from the first layer, we set a different z position to the data in the second subnetwork from that in the first one.

private function createGroup2():void{
        var box:ElementBox = network.elementBox;
    var bus:Node = NodeUtils.buildBus(new Vector3D(startx,ly,b1z),
        new Vector3D(b1xr,ly,b1z),12,0xa0a0a0);
    box.add(bus);
    box.add(NodeUtils.buildNodeWithCommon(gpg(-15,b2,0),
        serverSize,s1,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(-13,b2,0),
        serverSize,s1,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(-11,b2,0),
        serverSize,s1,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(-9,b2,0),
        serverSize,s1,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(-15,b2,7),
        serverSize,s1,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(-13,b2,7),
        serverSize,s1,null,null,null,"s"));
    box.add(NodeUtils.buildNodeWithCommon(gpg(-11,b2,7),
        serverSize,s1,null,null,null,"s"));
    box.add(NodeUtils.buildLink([gpg(-15,l2,0),gpg(-15,l2,4)],3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(-13,l2,0),gpg(-13,l2,4)],3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(-11,l2,0),gpg(-11,l2,4)],3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(-9,l2,0),gpg(-9,l2,4)],3,0x00AAA0));
    box.add(NodeUtils.buildBus(gpg(-16,l2,4),gpg(-8,l2,4),12,0xa0a0a0));
    box.add(NodeUtils.buildLink([gpg(-15,l2,7),gpg(-15,l2,4)],3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(-13,l2,7),gpg(-13,l2,4)],3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(-11,l2,7),gpg(-11,l2,4)],3,0x00AAA0));
    box.add(NodeUtils.buildLink([gpg(-9,l2,0),gpg(-9,ly,0),gpg(0,ly,0)],
        3,0x20AA20));
    var plane:Node = NodeUtils.buildPlane(new Vector3D(-800,100,300),
        new Vector3D(400,400,400),0xA0EEA0,"floor");
    box.add(plane);
    plane.setStyle(Style3D.ZORDERING_LAYER,-50);
    plane.setStyle(Style3D.BOTH_SIDES_VISIBLE,true);
    box.add(NodeUtils.createText(new Vector3D(-700,l2+20,100),
        "Group B",0xFFA0A0));
}

Now we get the result of the layers like this:
With the help of TWaver 3D library, we can rotate the scene by dragging mouse and zoom in and out by scrolling the mouse wheel, and even change the center of the view.

To Limit the Number of Choices in TList

We all know that TList in TWaver Java inherits from JList in Swing, so it is quite easy for us to control the selection mode, such as excusive choice, multiple choices and so on. But I will introduce to you in this article how to specify the number of choices available. Life involves choices constantly. Therefore, we have to make choices discreetly, whether the method SelectionModel of TList or way of life.
In actual projects, we often need to limit the number of the choices available in a list. The key may be the simplest one mentioned in our website: the method SelectionModel of DataBox. When the number of choices has surpassed the specified one, the earliest choices will be deleted.
box.getSelectionModel().addDataBoxSelectionListener(new DataBoxSelectionListener() {
    @Override
    public void selectionChanged(DataBoxSelectionEvent e) {
        if (e.getBoxSelectionModel().size() > max)
            e.getBoxSelectionModel().firstElement().setSelected(false);
        }
    }
});
We can set the property max. If max=1, then it becomes an exclusive selection.
In the following, a simple example has been made according to the above way: making a simple order list. There must be some similar scenarios in systems you have made: letting your users choose the specified number of data in list.

package test;

import java.awt.*;
import java.awt.event.*;
import java.util.*;

import javax.swing.*;

import twaver.*;
import twaver.list.*;

public class TestFrame extends JFrame {
    public TestFrame() {
        UIManager.put("Label.font", new Font("Dialog", Font.BOLD, 12));
        UIManager.put("OptionPane.font", new Font("Dialog", Font.BOLD, 12));
        this.setTitle("TWaver's menu orderer");
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setSize(700, 350);
        TWaverUtil.centerWindow(this);
        JPanel optionPane = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 120));
        final JComboBox cbMax = new JComboBox();
        for (int i = 1; i < 6; i++) {
            cbMax.addItem(i);
        }
        optionPane.add(new JLabel("At most you could order"));
        optionPane.add(cbMax);
        optionPane.add(new JLabel("dishes.    "));
        optionPane.add(new JLabel("The dishes you have ordered:"));
        final JLabel lbOptions = new JLabel("          ");
        optionPane.add(lbOptions);
        JButton btnOption = new JButton("order");
        optionPane.add(btnOption);
        btnOption.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                ArrayList<String> options = pickOption((Integer) cbMax.getSelectedItem());
                String text = "";
                for (String option : options) {
                    text += option + "  ";
                }
                lbOptions.setText(text);
            }
        });
        this.getContentPane().add(optionPane, BorderLayout.CENTER);
    }

    private ArrayList<String> pickOption(final int max) {
        String[] options = {
                "double-cooked pork slices",
                "Kung Pao Chicken",
                "Sauteed Shredded Pork with Sweet - Bean Sauce",
                "Braised chicken giblets",
                "Braised large intestine",
                "green pepper and scrambled eggs",
                "stir-fried eggs with tomato",
        };

        TDataBox box = new TDataBox();
        for (String option : options) {
            ResizableNode node = new ResizableNode();
            node.setName(option);
            box.addElement(node);
        }
        box.getSelectionModel().addDataBoxSelectionListener(new DataBoxSelectionListener() {
            @Override
            public void selectionChanged(DataBoxSelectionEvent e) {
                if (e.getBoxSelectionModel().size() > max) {
                    e.getBoxSelectionModel().firstElement().setSelected(false);
                }
            }
        });
        TList list = new TList(box);
        list.setTListSelectionMode(TList.CHECK_SELECTION);
        list.setIconVisible(false);
        JScrollPane scroll = new JScrollPane(list);
        Object[] message = new Object[] { "What would you like? Notice: You could order at most " + max + " dishes", scroll };
        int answer = JOptionPane.showConfirmDialog(this, message, "order", JOptionPane.OK_CANCEL_OPTION);
        ArrayList<String> result = new ArrayList<String>();
        if (answer == JOptionPane.OK_OPTION) {
            Iterator it = box.getSelectionModel().selection();
            while (it.hasNext()) {
                result.add(((Element) it.next()).getName());
            }
        }
        return result;
    }

    public static void main(String[] args) throws Exception {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        TestFrame ui = new TestFrame();
        ui.setVisible(true);
    }
}

2012/07/12

.NET Realizing the Four-Layer Topological Structure

TWaver Java can present network topology with a wonderful effect. Recently, some clients have made requests that they want to present it by .NET. It will not be that difficult with the help of the powerful functions of TWaver and the previous examples of Swing. Now I will share with you this method.


It’s a typical four-layer network. The first way I thought of is to realize it by Group. We can set the corresponded properties of group as in the previous examples of Swing.
group.IsExpanded = true;
group.SetStyle(Styles.BODY_ALPHA, 0.8);
group.SetStyle(Styles.GROUP_FILL_COLOR, color);
group.SetStyle(Styles.GROUP_GRADIENT, Consts.GRADIENT_LINEAR_NORTHEAST);
group.SetStyle(Styles.LABEL_POSITION, Consts.POSITION_RIGHT);
group.SetStyle(Styles.LABEL_XOFFSET, 100);
group.SetStyle(Styles.LABEL_SIZE, 14);
group.SetStyle(Styles.LABEL_BOLD, true);
group.SetStyle(Styles.LABEL_FONT, new FontFamily("微软雅黑"));
This rectangle is not agreeable. We have found out that there seems to be no property to set parallelogram after reading TWaver's relating documents. This is quite troublesome, for we have to turn to the customized functions for help. Fortunately, thanks to TWaver’s user-friendliness, we can repaint it with the corresponded interface. In order to show the parallelogram with 3D effects, it is necessary to set two properties to 3D group: one being angle, which is the offset location of the upper two corners of the group (a parallelogram) in relation to the two lower corners of the rectangle. The other is depth, that is, the thickness of 3D. Now we are able to customize it with the two parameters. We need to rewrite group's method DrawContent(). First, we can draw the dots at the four corners of the parallelogram according to angle with the method bound from Group.
double angle = group.Angle;
double deep = group.Deep;
double gap = bounds.Height / Math.Tan(angle * Math.PI / 180);
PointCollection points = new PointCollection();
Point p1 = new Point(bounds.X - gap, bounds.Y + bounds.Height);
points.Add(p1);
Point p2 = new Point(bounds.X + bounds.Width, bounds.Y + bounds.Height);
points.Add(p2);
Point p3 = new Point(bounds.X + bounds.Width + gap, bounds.Y);
points.Add(p3);
points.Add(new Point(bounds.X, bounds.Y));
// Add
path.Data = this.CreateGeometry(points);
this.AddComponent(path);
this.AddBodyBounds(path.Data.Bounds);
Next, we need to draw the 3D effects’ parts below and at the right side: draw a small parallelogram below. This is quite easy as long as we calculate accurately the position of the dots at the four corners of the parallelogram through the two dots at the two lower corners of the bigger parallelogram and the thickness of it.
// Add Bottom Rectangle
path = new Path();
// Set Fill
path.Fill = Utils.CreateSolidColorBrush(fillColor);
// Set Outline
this.SetStroke(path);
// Set Data
RectangleGeometry rectangleGeometry = new RectangleGeometry();
rectangleGeometry.Rect = new Rect(p1.X, p1.Y, p2.X-p1.X, deep);
// Add
path.Data = rectangleGeometry;
this.AddComponent(path);
this.AddBodyBounds(path.Data.Bounds);
Here the details to the parallelogram at the right side are omitted since it is the same as the parallelogram below. Let’s see the running effects:

The effect is quite agreeable as we have expected. Since the parallelogram is ready, we will add some nodes onto it.
Continue to create the four groups with the general method.

Now we will try to make the highlight in this Demo: the penetrating effect. This means that how can the links linking different layers penetrate group when the node at an upper layer is related to another node at another layer, which has something to do with "layer" in TWaver. The relationship among the nodes in the four above structures is shown through layers. The software business layer is set as the top one, overlapping elements at all of the layers below; the same as the penetrating of links. We can divide a link as two parts: the upper part, which is at the same layer as the from-nodes and the lower part, which is at the same layer as the to-nodes. The two parts are linked with a virtual node in the middle.
private void CreateCrossLayerLink(object fromID, object toID, bool left)
        {
            Node from = (Node)this.box.GetDataByID(fromID);
            Node to = (Node)this.box.GetDataByID(toID);
            if (from == null || to == null)
            {
                return;
            }
            ConnectPoint point = new ConnectPoint();
            point.LayerID = from.LayerID;
            double xOffset = -from.Width / 2 - 15;
            if (left)
            {
                xOffset = from.Width / 2 + 15;
            }
            double yOffset = from.Height / 2 + 20;
            double x = from.CenterLocation.X + xOffset;
            double y = from.CenterLocation.Y + yOffset;
            point.SetCenterLocation(x, y);
            point.Parent = from.Parent;
            this.box.Add(point);

            LayerLink upLink = new LayerLink(from, point, true);
            upLink.LayerID = from.LayerID;
            upLink.Parent = from.Parent;
            this.box.Add(upLink);

            LayerLink downLink = new LayerLink(point, to, false);
            downLink.LayerID = to.LayerID;
            downLink.Parent = to.Parent;
            this.box.Add(downLink);
        }
The effect is seen immediately. Let’s create more links.