Saturday, November 28, 2009

how to highlight syntax in source code cited in google blog

It is easy to highlight source code syntax in your blog. The key is an open source project called SyntaxHighlighter. It is a wonderful tool to help you to highlight more than 20 computer languages in the Web page. We only need to highlighted syntax for source code cited in your web page,
  1. Include/link Syntaxhighlighter's Javascript file and CSS file in your web page.
  2. put the source code in tag pair <pre class="brush: java"></pre>
    . brush can be Java, PHP, SQL and more than twenty language names.
In case of google blog, we can enable SyntaxHighlighter in the following steps too,
  1. login your google blogger and navigate with path Design --> Edit HTML 
  2. insert the HTML tags in bottoms in the HTML template. I inserted it in the tag in order to make sure these scripts are loaded before the page will be displayed. But, you can insert it in other place too. 
  3. surround your source code with <pre> tag and specify proper brush name according to your source code.

Wednesday, November 18, 2009

A simple Xlet console for debugging log

I made this Xlet when I did Xlet programming as I feel there should be something like debug console in STB. So, I find a AWT scrollableText and modify it to be an Xlet. So, I have a debug console that can be shown on big TV screen. I can know what happen in the Xlet in real time and real STB environment. This code is a little bit rough. But it is a handy tool I like. However, I must emphasize to MHP programmer who want to use this. That is, MHP specification does not require MHP environment supports AWT API although many MHP vendor ported a JVM with AWT package into their STB. So, you don't need to be surprised if this code does not work in certain STB. But, I know this will rarely happen. Enjoy it.

/*
 * ScrollableText.java
 *
 * Created on September 22, 2005
 *
 */

import java.awt.Font;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Insets;
import java.awt.AWTEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;

import org.havi.ui.*;

/**
 * ScrollableText.java
 *
 * A text component that can function as a stand-alone
 * scrollable test displayer. Contains the neccessary
 * methods for processing user input.
 *
 */
public class ScrollableText extends HComponent{
    
    
    
    /** The alignment styles */
    public static final int ALIGN_LEFT = 0;
    public static final int ALIGN_RIGHT = 1;
    public static final int ALIGN_CENTER = 2;
    public static final int ALIGN_TOP = 3;
    public static final int ALIGN_BOTTOM = 4;
    
    /** the keys used for navigating */
    public static final int UP_KEY = KeyEvent.VK_UP;
    public static final int DOWN_KEY = KeyEvent.VK_DOWN;
    public static final int PGUP_KEY = KeyEvent.VK_LEFT;
    public static final int PGDOWN_KEY = KeyEvent.VK_RIGHT;
    
    /** Directional consts */
    public static final int DIR_UP = 1;
    public static final int DIR_DOWN = 2;
    
    /** Position tokens */
    public static final int POS_BEGINNING = 0;
    public static final int POS_END = -1;
    
    /** The width of the sidescroller */
    public static final int SIDESCROLLER_WIDTH = 20;
    
    /** The sidescroller show- policy */
    public static final int SIDESCROLLER_ALWAYS = 0;
    public static final int SIDESCROLLER_NEVER = 1;
    public static final int SIDESCROLLER_AS_NEEDED = 2;
    
    /** Default color scheme */
    private Color SCROLLER_BG = new Color(24, 93, 184);
    private Color SCROLLER_TAB_BG = new Color(110, 171, 102);
    private Color SCROLLER_ACTIVE_ARROW = new Color(110, 171, 102);
    private Color SCROLLER_INACTIVE_ARROW = new Color(24, 93, 184);
    private Color SCROLLER_ARROW_OUTLINE = Color.black;
    private Color SCROLLER_HIGHLIGHT = Color.white;
    private Color SCROLLER_SHADOW = Color.black;
    //private Color foreground = Color.black;
    //private Color background = Color.white;
    
    /** The sidescroller policy in use */
    private int sidescrollerPolicy;
    
    /** Indicates if the sidescroller should be painted */
    private boolean sidescroller;
    
    /* The size of the scrollbar */
    private int scrollerHeight;
    
    /** The positional multiplier */
    private float scrollerUnit;
    
    private static final double[] UP_ARROW_X = { 0.5, 0, 0.3, 0.3, 0.7, 0.7, 1 };
    private static final double[] UP_ARROW_Y = { 0, 0.7, 0.7, 1, 1, 0.7, 0.7 };
    
    private static final double[] DOWN_ARROW_X = { 0.3, 0.3, 0, 0.5, 1, 0.7, 0.7 };
    private static final double[] DOWN_ARROW_Y = { 0, 0.3, 0.3, 1, 0.3, 0.3, 0 };
    
    private static final int UP_ARROW = 0;
    private static final int DOWN_ARROW = 1;
    
    /** Variables related to the Scrollable interface */
    private int scrollUnit;
    private int scrollBlock;
    private int scrollBy;
    private int totalHeight;
    
    /** Presentation- related values */
    private String text;
    private int lineSpace;
    
    /** Margins */
    private int rMarg;
    private int lMarg;
    private int tMarg;
    private int bMarg;
    
    /** The alignation style */
    private int align;
    private int valign;
    
    /** The y- positions of the rows */
    private int rowPos[];
    
    /** The x- positions of the individual rows */
    private int rowIndent[];
    
    /** The row that is displayed on the top of the area */
    private int currentRow;
    private int displayRows;
    
    /** The splitup of the text to paint */
    private String rows[];
    
    
    /** Supports transparent background */
    private boolean transparent;
    
    /** Creates a new instance of ScrollableText */
    public ScrollableText() {
        this.enableEvents(AWTEvent.KEY_EVENT_MASK);
        this.scrollBy = 1;
    }
    
    
    /**
     * The complete constructors.
     * Additional information should be set using the default java.awt.Component
     * methods; setFont(), setForeground(), setBackground(), setSize(), setLocation();
     *
     * @param insets The margins
     * @param align The horizontal alignment of the text
     * @param valign The vertical alignment of the text
     * @param sidescroller The sidescroller policy
     */
    public ScrollableText(Insets insets, int align, int valign, int sidescroller) {
        
        super();
        this.enableEvents(AWTEvent.KEY_EVENT_MASK);
        this.align = align;
        this.valign = valign;
        this.text = text;
        this.sidescrollerPolicy = sidescroller;
        this.scrollBy = 1;
        
        this.lMarg = insets.left;
        this.rMarg = insets.right;
        this.tMarg = insets.top;
        this.bMarg = insets.bottom;
        
        this.transparent = true;
        text = "";
    }
/*
    public void setForeground(org.dvb.ui.DVBColor foreground){
        this.foreground = foreground;
        super.setForeground(foreground);
    }
 
    public void setBackground(org.dvb.ui.DVBColor background){
        this.background = background;
        super.setBackground(background);
    }
 */
    /**
     * Changes the text within this component
     * @param text The new contents
     */
    public void setText(String text) {
        
        this.currentRow = 0;
        this.text = text;
        int fontSize = 0;
        Font font = this.getFont();
        
        int width = this.getSize().width;
        int height = this.getSize().height;
        
        //FontMetrics fm = this.getFontMetrics(new Font("SansSerif", Font.PLAIN, 24) );
        FontMetrics fm = null;
        if (font != null) {
            fm = this.getFontMetrics(font);
            fontSize = font.getSize();
            
            if (this.sidescrollerPolicy != SIDESCROLLER_NEVER)
                this.rows = convertText(text, width - (this.rMarg + this.lMarg +
                        SIDESCROLLER_WIDTH),
                        fm);
            else
                this.rows = convertText(text, width - (this.rMarg + this.lMarg), fm);
            
        } else {
            this.rows = new String[1];
            this.rows[0] = text;
        }
        
        if (fm != null)
            this.lineSpace = fm.getDescent();
        else
            this.lineSpace = (int)(fontSize/4);
        
        this.totalHeight = (fontSize + this.lineSpace) * this.rows.length;
        this.scrollBlock = fontSize + this.lineSpace;
        this.scrollUnit = fontSize + this.lineSpace;
        
        int usableHeight = height - this.tMarg - this.bMarg;
        if ((usableHeight < 1) || (this.scrollBlock < 1)) {
            this.displayRows = 0;
        } else {
            this.displayRows = usableHeight / this.scrollBlock;
            
            if (this.displayRows > this.rows.length)
                this.displayRows = this.rows.length;
            
            /* vertical alignment */
            int valignOfs = 0;
            if (this.valign != ALIGN_TOP) {
                valignOfs = height - this.displayRows * this.scrollBlock;
                /* valignOfs is currently suitable for ALIGN_BOTTOM */
                if (this.valign == ALIGN_CENTER)
                    valignOfs = (int)(valignOfs/2);
            }
            
            /* Calculate the positions of the rows beforehand */
            this.rowPos = new int[ this.displayRows ];
            int posCount = fm.getAscent() + this.tMarg + valignOfs;
            for(int i = 0; i < this.displayRows; i++) {
                
                this.rowPos[i] = posCount;
                posCount += this.scrollBlock;
            }
        }
        
        float sHeight = height - (2 * SIDESCROLLER_WIDTH) - 2;
        
        /* Calculate sidescroller data */
        if (this.rows.length <= this.displayRows) {
            this.scrollerUnit = 0;
            this.scrollerHeight = (int)sHeight;
        } else {
            
            this.scrollerUnit = (sHeight / (float)this.rows.length);
            this.scrollerHeight = Math.round(((float)this.displayRows * this.scrollerUnit));
        }
        
        /* Make sure the scroller has some size */
        if (this.scrollerHeight < 3)
            this.scrollerHeight = 4;
        
        switch(this.sidescrollerPolicy) {
            case SIDESCROLLER_ALWAYS:
                this.sidescroller = true;
                break;
            case SIDESCROLLER_NEVER:
                this.sidescroller = false;
                break;
            case SIDESCROLLER_AS_NEEDED:
            default:
                if (this.scrollerUnit > 0)
                    this.sidescroller = true;
                else
                    this.sidescroller = false;
        }
        
    }
    
    
    
    /**
     * Returns the start of a line if using the set alignation style
     * and the given width
     */
    private int getIndent(int strWidth, int width) {
        
        switch(this.align) {
            
            case ALIGN_RIGHT:
                return width - strWidth;
            case ALIGN_CENTER:
                return (width - strWidth) / 2;
            default:
                return 0;
                
        }
    }
    
    
    /**
     * Small method to add a string to one Vector and it's
     * indentation information to the other
     */
    private void addStr(String str, Vector strings, Vector indents,
            FontMetrics fm, int width) {
        
        strings.addElement(str);
        indents.addElement(new Integer(getIndent(fm.stringWidth(str), width)));
    }
    
    
    /**
     * Converts the given string into an array of String with each entry
     * having the width of width or less.
     *
     */
    private String[] convertText(String text, int width, FontMetrics fm) {
        
        /* Check one line at a time, add it to the final */
        Vector al = new Vector();
        Vector inList = new Vector();
        String tmp = "";
        for(int i = 0; i < text.length(); i++) {
            
            char ch = text.charAt(i);
            
            /* Check for control chars */
            switch(ch) {
                case '\n':
                    
                    addStr(tmp, al, inList, fm, width);
                    tmp = "";
                    break;
                case '\t':
                    tmp += "   ";
                    break;
                default:
                    tmp += ch;
                    
                    try {
                        
                        if (fm.stringWidth(tmp) > width) {
                            
                            int pos = ScrollableText.getBreakPos(tmp);
                            if (pos > 0) {
                                
                                addStr(tmp.substring(0, pos), al, inList, fm, width);
                                tmp = tmp.substring(pos+1, tmp.length());
                            } else {
                                /* No breaking possible; remove last character */
                                addStr(tmp.substring(0, tmp.length()-1), al, inList, fm, width);
                                tmp = tmp.charAt(tmp.length()-1) + "";
                            }
                            
                        }
                        
                    } catch(Exception e) {
                        System.out.println("ScrollableText.convertText exception: " + e);
                        e.printStackTrace(System.out);
                        String ret[] = new String[1];
                        ret[0] = text;
                        return ret;
                    }
            }
        }
        
        if (tmp.length() > 0)
            addStr(tmp, al, inList, fm, width);
        
        String[] ret = new String[ al.size() ];
        rowIndent = new int[ inList.size() ];
        for(int i = 0; i < al.size(); i++) {
            ret[i] = (String)al.elementAt(i);
            rowIndent[i] = ((Integer)inList.elementAt(i)).intValue();
        }
        
        return ret;
    }
    
    
    /**
     * Returns the position where the given String can be
     * broken up according to spaces and tabs, starting from
     * the end of the String.
     * If such is not found before a newline or at all, the
     * resulting value will be negative. If found, the resulting
     * value is the position of the breaking character, which
     * can safely be substituted for a newline, eg.
     *  String new = old.substring(0, ret_value) + '\n'
     *               + old.substring(ret_value+1, old.length());
     *
     */
    private static int getBreakPos(String str) {
        
        for(int i = str.length() - 1; (i > -1) && (str.charAt(i) != '\n'); i--) {
            
            if ((str.charAt(i) == ' ') ||
                    (str.charAt(i) == '\t')) {
                
                /* A suitable position is found */
                return i;
            }
        }
        
        /* Not found */
        return -1;
    }
    
    
    /**
     * Changes the size of this component
     */
    public void setSize(int width, int height) {
        
        super.setSize(width, height);
        
        setText(this.text);
    }
    
    
    /**
     * Changes the size of this component
     */
    public void setSize(Dimension dim) {
        this.setSize(dim.width, dim.height);
    }
    
    
    /**
     * Changes the font of this component
     */
    public void setFont(Font font) {
        
        super.setFont(font);
        this.setText(this.text);
    }
    
    
    /**
     * Sets the background of the text - supports transparent
     * by providing null
     */
    public void setBackground(Color bg) {
        
        super.setBackground(bg);
        
        this.transparent = (bg == null);
    }
    
    
    
    /**
     * Sets the color scheme for the sidescroller
     *
     */
    public void setScrollerColors(Color bg, Color tabBg,
            Color activeArrow, Color inactiveArrow,
            Color arrowOutline, Color highlight,
            Color shadow) {
        
        SCROLLER_BG = bg;
        SCROLLER_TAB_BG = tabBg;
        SCROLLER_ACTIVE_ARROW =  activeArrow;
        SCROLLER_INACTIVE_ARROW = inactiveArrow;
        SCROLLER_ARROW_OUTLINE = arrowOutline;
        SCROLLER_HIGHLIGHT = highlight;
        SCROLLER_SHADOW = shadow;
        
        this.repaint();
    }
    
    
    /**
     * Changes multiple properties of this component
     *
     * @param text The text to be contained within the
     *             component. If null, the old text will
     *             be preserved.
     */
    public void setPresentation(int x, int y, int width, int height,
            int align, int valign, String text) {
        
        this.setBounds(x, y, width, height);
        
        this.align = align;
        this.valign = valign;
        
        if (text != null)
            this.text = text;
        
        setText(this.text);
    }
    
    
    
    /**
     * Changes the horizontal alignment of this component
     *
     * @param alignment One of the Horizontal alignment
     *                  constants; ALIGN_LEFT ALIGN_RIGHT ALIGN_CENTER
     */
    public void setHorizontalAlignment(int alignment) {
        
        this.align = alignment;
        setText(this.text);
    }
    
    
    /**
     * Changes the vertical alignment of this component
     * @param alignment One of the Vertical alignment
     *                  constants; ALIGN_TOP ALIGN_BOTTOM ALIGN_CENTER
     */
    public void setVerticalAlignment(int alignment) {
        
        this.valign = alignment;
        setText(this.text);
    }
    
    /**
     * Sets the number or rows the component should
     * be scrolled by with each scroll
     * @param rows The number of rows
     */
    public void setScrollRows(int rows) {
        
        this.scrollUnit = rows * this.scrollBlock;
        this.scrollBy = rows;
    }
    
    /**
     * Scrolls the component one scrollunit in the
     * given direction
     * @param direction The direction; DIR_UP, DIR_DOWN
     */
    public void scrollContents(int direction) {
        
        if (direction == DIR_DOWN)
            this.currentRow -= this.scrollBy;
        else
            this.currentRow += scrollBy;
        
        checkState();
    }
    
    /**
     * Scrolls the component one visible page (if available)
     * in the given direction
     * @param direction The direction of the scroll
     */
    public void scrollPage(int direction) {
        
        if (direction == DIR_DOWN)
            this.currentRow -= this.displayRows;
        else
            this.currentRow += this.displayRows;
        
        checkState();
    }
    
    
    /**
     * Jumps to the given position (in rows) within
     * the contents.
     * @param position The position, or a special code:
     *                 POS_BEGINNING, POS_END
     */
    public void jumpTo(int position) {
        
        switch(position) {
            case POS_BEGINNING:
                this.currentRow = 0;
                break;
            case POS_END:
                this.currentRow = this.rows.length;
                break;
            default:
                this.currentRow = position;
        }
        
        checkState();
    }
    
    
    
    /**
     * Returns the total number of rows contained within
     * the object
     * @return The total number of rows
     */
    public int getTotalScrollBlocks() {
        
        return this.rows.length;
    }
    
    /**
     * Returns the total number of rows that fits
     * within the components boundaries
     * @return The number of visible rows
     */
    public int getVisibleScrollBlocks() {
        
        return this.displayRows;
    }
    
    /**
     * Returns the number of hidden rows above the
     * visible area
     * @return The number of not- visible, scrolled by rows
     */
    public int getHiddenTopScrollBlocks() {
        
        return this.currentRow;
    }
    
    /**
     * Returns the number of hidden rows below the
     * visible area
     * @return The number of hidden, unviewed rows
     */
    public int getHiddenBottomScrollBlocks() {
        
        int ret = this.rows.length - this.displayRows - this.currentRow;
        if (ret < 0)
            ret = 0;
        
        return ret;
    }
    
    
    /**
     * Method that does sanity- checking on presentation- related
     * conditions before calling paint
     */
    private void checkState() {
        
        /* Check position */
        if (this.currentRow > (this.rows.length -  this.displayRows))
            this.currentRow = this.rows.length - this.displayRows;
        
        if (this.currentRow < 0)
            this.currentRow = 0;
        
        this.repaint();
    }
    
    /**
     * Paints the text
     */
    public void paint(Graphics g) {
        
        if (this.isVisible()) {
            
            if (!this.transparent) {
                g.setColor(this.getBackground());
                g.fillRect(0, 0, this.getSize().width, this.getSize().height);
            }
            
            g.setFont(this.getFont());
            //this.setForeground(Color.b)
            g.setColor(this.getForeground());
            //g.setColor(Color.BLACK);
            
            for(int i = 0; i < this.displayRows; i++) {
                g.drawString(this.rows[i+this.currentRow],
                        this.lMarg + this.rowIndent[this.currentRow+i], this.rowPos[i]);
            }
            
            if (this.sidescroller) {
                drawScroller(g);
            }
            
            
        }
    }
    
    
    /**
     * Draws the scrollbar
     */
    private void drawScroller(Graphics g) {
        
        int width = this.getSize().width;
        int height = this.getSize().height;
        
        int startx = width - SIDESCROLLER_WIDTH;
        int barSY = SIDESCROLLER_WIDTH + 2 + (int)((float)this.currentRow * this.scrollerUnit);
        int barEY = barSY + this.scrollerHeight-2;
        
        /* Make sure the scrollbar has some height, and is within the boundaries */
        int dfix = barEY - (height - SIDESCROLLER_WIDTH - 2);
        if (dfix > 0) {
            
            if ((barSY - dfix) < SIDESCROLLER_WIDTH)
                barSY = SIDESCROLLER_WIDTH +1;
            else
                barSY = barSY - dfix;
            
            barEY = barEY - dfix;
        }
        
        g.setColor(SCROLLER_BG);
        g.fillRect(startx, 0, this.SIDESCROLLER_WIDTH, height);
        
        g.setColor(SCROLLER_TAB_BG);
        g.fillRect(startx+1, barSY, SIDESCROLLER_WIDTH - 2, (barEY-barSY));
        
        g.setColor(SCROLLER_SHADOW);
        g.drawLine(startx, this.SIDESCROLLER_WIDTH + 1, width-1, this.SIDESCROLLER_WIDTH + 1);
        g.drawLine(startx, this.SIDESCROLLER_WIDTH + 1, startx, height - this.SIDESCROLLER_WIDTH - 1);
        
        g.drawLine(width-2, barSY+1, width-2, barEY);
        g.drawLine(startx+1, barEY, width-2, barEY);
        
        g.setColor(SCROLLER_HIGHLIGHT);
        g.drawLine(width-1, this.SIDESCROLLER_WIDTH + 1,
                width-1, height - this.SIDESCROLLER_WIDTH - 1 );
        g.drawLine(startx, height - this.SIDESCROLLER_WIDTH - 1,
                width-1, height - this.SIDESCROLLER_WIDTH - 1);
        
        g.drawLine(startx+1, barSY, width-2, barSY);
        g.drawLine(startx+1, barSY, startx+1, barEY);
        
        
        if (getHiddenTopScrollBlocks() > 0) {
            drawArrow(UP_ARROW, startx, 0,
                    SIDESCROLLER_WIDTH, SIDESCROLLER_WIDTH, SCROLLER_ACTIVE_ARROW,
                    g);
            
        } else {
            drawArrow(UP_ARROW, startx, 0,
                    SIDESCROLLER_WIDTH, SIDESCROLLER_WIDTH, SCROLLER_INACTIVE_ARROW,
                    g);
        }
        
        if (getHiddenBottomScrollBlocks() > 0) {
            drawArrow(DOWN_ARROW, startx, height - SIDESCROLLER_WIDTH,
                    SIDESCROLLER_WIDTH, SIDESCROLLER_WIDTH, SCROLLER_ACTIVE_ARROW,
                    g);
            
        } else {
            drawArrow(DOWN_ARROW, startx, height - SIDESCROLLER_WIDTH,
                    SIDESCROLLER_WIDTH, SIDESCROLLER_WIDTH, SCROLLER_INACTIVE_ARROW,
                    g);
        }
        
        
    }
    
    
    /**
     * Draws an arrow in the given direction
     *
     */
    private void drawArrow(int arrow, int x, int y, int width, int length,
            Color color, Graphics g) {
        
        int[] realX = new int[ 7 ];
        int[] realY = new int[ 7 ];
        
        double[] srcX;
        double[] srcY;
        
        switch(arrow){
            case UP_ARROW:
                srcX = UP_ARROW_X;
                srcY = UP_ARROW_Y;
                break;
            case DOWN_ARROW:
            default:
                srcX = DOWN_ARROW_X;
                srcY = DOWN_ARROW_Y;
                break;
        }
        
        for(int i = 0; i < srcX.length; i++) {
            realX[i] = x + (int)(width * srcX[i]);
        }
        
        for(int i = 0; i < srcY.length; i++) {
            realY[i] = y + (int)(length * srcY[i]);
        }
        
        g.setColor(color);
        g.fillPolygon(realX, realY, realX.length);
        
        /* draw outline */
        g.setColor(SCROLLER_ARROW_OUTLINE);
        g.drawPolygon(realX, realY, realX.length);
        
    }
    
    
    /**
     * Declears the scrollpanel to be focusable
     *
     */
    public boolean isFocusable() {
        
        return true;
    }
    
    
    /**
     * Handles user input
     */
    
    public void processKeyEvent(KeyEvent e) {
        
        if (this.isVisible()) {
            
            switch(e.getKeyCode()) {
                case UP_KEY:
                    this.scrollContents(DIR_DOWN);
                    break;
                case DOWN_KEY:
                    this.scrollContents(DIR_UP);
                    break;
                case PGUP_KEY:
                    this.scrollPage(DIR_DOWN);
                    break;
                case PGDOWN_KEY:
                    this.scrollPage(DIR_UP);
                    break;
                default:
            }
            
        }
    }
    
}

Tuesday, November 10, 2009

Implementing Rich Internet Application as a desktop application

We have been talking about Rich Internet Application for years now. However, have you even thought about using RIA to implement a desktop application? I have had this idea for years. I think it will be wonderful for me to share this with others. Below is a high level architecture design for this.

A cutting age desktop application architecture design.
The benefit of this design is clear,
  1.  This will be more than a desktop application. It will be a service running on Windows or Linux.
  2. Once this framework is done. Only java Web developers are needed as manpower resource. 
  3. As generally accepted, compared with using traditional desktop widget toolkit, it will be easier for developer to make GUI with JavaScript toolkits like ExtJS, Dojo, jQuery etc. We all have wonderful experienced with modern Web user interface.
  4. As both server and client is located on user's desktop, many security restriction can be walked around. Our standalone java application can still do what ever it wants to do. 
  5. JRE is free. jetty is free. They are even free for redistribution.
  6. Furthermore, I can make a customized web browser to let user feel that they are just using a beautiful desktop application, which is fully created by me. \
Isn't this design interesting? Contact with me for more detailed technical design.