DiffViewManager.java :  » IDE-Netbeans » diff » org » netbeans » modules » diff » builtin » visualizer » editable » Java Open Source

Java Open Source » IDE Netbeans » diff 
diff » org » netbeans » modules » diff » builtin » visualizer » editable » DiffViewManager.java
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s):
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
 * Microsystems, Inc. All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */
package org.netbeans.modules.diff.builtin.visualizer.editable;

import org.netbeans.api.diff.Difference;
import org.netbeans.editor.*;
import org.netbeans.editor.Utilities;
import org.netbeans.editor.BaseDocument;
import org.netbeans.spi.editor.highlighting.HighlightsContainer;
import org.netbeans.spi.diff.DiffProvider;
import org.openide.util.Lookup;

import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import javax.swing.*;
import javax.swing.text.*;
import java.awt.Dimension;
import java.util.*;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.io.*;

/**
 * Handles interaction among Diff components: editor panes, scroll bars, action bars and the split pane.
 * 
 * @author Maros Sandor
 */
class DiffViewManager implements ChangeListener {
    
    private final EditableDiffView master;
    
    private final DiffContentPanel leftContentPanel;
    private final DiffContentPanel rightContentPanel;

    /**
     * True when this class caused the current scroll event, false otherwise.
     */ 
    private boolean myScrollEvent;
    
    private int                     cachedDiffSerial;
    private DecoratedDifference []  decorationsCached = new DecoratedDifference[0];
    private HighLight []            secondHilitesCached = new HighLight[0];
    private HighLight []            firstHilitesCached = new HighLight[0];
    private final ScrollMapCached   scrollMap = new ScrollMapCached();    
    
    public DiffViewManager(EditableDiffView master) {
        this.master = master;
        this.leftContentPanel = master.getEditorPane1();
        this.rightContentPanel = master.getEditorPane2();
    }
    
    void init() {
        initScrolling();
    }

    private void initScrolling() {
        leftContentPanel.getScrollPane().getVerticalScrollBar().getModel().addChangeListener(this);
        rightContentPanel.getScrollPane().getVerticalScrollBar().getModel().addChangeListener(this);
        // The vertical scroll bar must be there for mouse wheel to work correctly.
        // However it's not necessary to be seen (but must be visible so that the wheel will work).
        leftContentPanel.getScrollPane().getVerticalScrollBar().setPreferredSize(new Dimension(0, 0));
    }

    private final Boolean [] smartScrollDisabled = new Boolean[] { Boolean.TRUE };
    
    public void runWithSmartScrollingDisabled(Runnable runnable) {
        synchronized (smartScrollDisabled) {
            smartScrollDisabled[0] = true;
        }
        try {
            runnable.run();
        } catch (Exception e) {
            Logger.getLogger(DiffViewManager.class.getName()).log(Level.SEVERE, "", e);
        } finally {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    synchronized (smartScrollDisabled) {
                        smartScrollDisabled[0] = false;
                    }
                }
            });
        }
    }
    
    public void stateChanged(ChangeEvent e) {
        JScrollBar leftScrollBar = leftContentPanel.getScrollPane().getVerticalScrollBar();
        JScrollBar rightScrollBar = rightContentPanel.getScrollPane().getVerticalScrollBar();
        if (e.getSource() == leftContentPanel.getScrollPane().getVerticalScrollBar().getModel()) {
            int value = leftScrollBar.getValue();
            leftContentPanel.getActionsScrollPane().getVerticalScrollBar().setValue(value);
            if (myScrollEvent) return;
            myScrollEvent = true;
        } else {
            int value = rightScrollBar.getValue();
            rightContentPanel.getActionsScrollPane().getVerticalScrollBar().setValue(value);
            if (myScrollEvent) return;
            myScrollEvent = true;
            boolean doSmartScroll;
            synchronized (smartScrollDisabled) {
                doSmartScroll = !smartScrollDisabled[0];
            }
            if (doSmartScroll) {
                smartScroll();
                master.updateCurrentDifference();
            }
        }
        master.getMyDivider().repaint();
        myScrollEvent = false;
    }
    
    public void scroll() {
        myScrollEvent = true;
        smartScroll();
        master.getMyDivider().repaint();
        myScrollEvent = false;
    }
    
    EditableDiffView getMaster() {
        return master;
    }
    
    private void updateDifferences() {
        assert Thread.holdsLock(this);
        int mds = master.getDiffSerial();
        if (mds <= cachedDiffSerial) return;
        cachedDiffSerial = mds;
        computeDecorations();
        computeSecondHighlights();
        computeFirstHighlights();
    }

    public synchronized DecoratedDifference [] getDecorations() {
        updateDifferences();
        return decorationsCached;
    }

    public synchronized HighLight[] getSecondHighlights() {
        updateDifferences();
        return secondHilitesCached;
    }

    public synchronized HighLight[] getFirstHighlights() {
        updateDifferences();
        return firstHilitesCached;
    }
    
    private void computeFirstHighlights() {
        List<HighLight> hilites = new ArrayList<HighLight>();
        Document doc = leftContentPanel.getEditorPane().getDocument();
        for (DecoratedDifference dd : decorationsCached) {
            Difference diff = dd.getDiff();
            if (dd.getBottomLeft() == -1) continue;
            int start = getRowStartFromLineOffset(doc, diff.getFirstStart() - 1);
            if (isOneLineChange(diff)) {
                CorrectRowTokenizer firstSt = new CorrectRowTokenizer(diff.getFirstText());
                CorrectRowTokenizer secondSt = new CorrectRowTokenizer(diff.getSecondText());
                for (int i = diff.getSecondStart(); i <= diff.getSecondEnd(); i++) {
                    String firstRow = firstSt.nextToken();                 
                    String secondRow = secondSt.nextToken();                 
                    List<HighLight> rowhilites = computeFirstRowHilites(start, firstRow, secondRow);
                    hilites.addAll(rowhilites);
                    start += firstRow.length() + 1;
                }
            } else {
                int end = getRowStartFromLineOffset(doc, diff.getFirstEnd());
                if (end == -1) {
                    end = doc.getLength();
                }
                SimpleAttributeSet attrs = new SimpleAttributeSet();
                StyleConstants.setBackground(attrs, master.getColor(diff));
                attrs.addAttribute(HighlightsContainer.ATTR_EXTENDS_EOL, Boolean.TRUE);
                hilites.add(new HighLight(start, end, attrs));
            }
        }
        firstHilitesCached = hilites.toArray(new HighLight[hilites.size()]);
    }
    
    static int getRowStartFromLineOffset(Document doc, int lineIndex) {
        if (doc instanceof BaseDocument) {
            return Utilities.getRowStartFromLineOffset((BaseDocument) doc, lineIndex);
        } else {
            // TODO: find row start from line offet
            Element element = doc.getDefaultRootElement();
            Element line = element.getElement(lineIndex);
            return line.getStartOffset();
        }
    }
    
    private void computeSecondHighlights() {
        List<HighLight> hilites = new ArrayList<HighLight>();
        Document doc = rightContentPanel.getEditorPane().getDocument();
        for (DecoratedDifference dd : decorationsCached) {
            Difference diff = dd.getDiff();
            if (dd.getBottomRight() == -1) continue;
            int start = getRowStartFromLineOffset(doc, diff.getSecondStart() - 1);
            if (isOneLineChange(diff)) {
                CorrectRowTokenizer firstSt = new CorrectRowTokenizer(diff.getFirstText());
                CorrectRowTokenizer secondSt = new CorrectRowTokenizer(diff.getSecondText());
                for (int i = diff.getSecondStart(); i <= diff.getSecondEnd(); i++) {
                    try {
                        String firstRow = firstSt.nextToken();
                        String secondRow = secondSt.nextToken();
                        List<HighLight> rowhilites = computeSecondRowHilites(start, firstRow, secondRow);
                        hilites.addAll(rowhilites);
                        start += secondRow.length() + 1;
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            } else {
                int end = getRowStartFromLineOffset(doc, diff.getSecondEnd());
                if (end == -1) {
                    end = doc.getLength();
                }
                SimpleAttributeSet attrs = new SimpleAttributeSet();
                StyleConstants.setBackground(attrs, master.getColor(diff));
                attrs.addAttribute(HighlightsContainer.ATTR_EXTENDS_EOL, Boolean.TRUE);
                hilites.add(new HighLight(start, end, attrs));
            }
        }
        secondHilitesCached = hilites.toArray(new HighLight[hilites.size()]);
    }

    private List<HighLight> computeFirstRowHilites(int rowStart, String left, String right) {
        List<HighLight> hilites = new ArrayList<HighLight>(4);
        
        String leftRows = wordsToRows(left);  
        String rightRows = wordsToRows(right);

        DiffProvider diffprovider = Lookup.getDefault().lookup(DiffProvider.class);
        if (diffprovider == null) {
            return hilites;
        }

        Difference[] diffs;
        try {
            diffs = diffprovider.computeDiff(new StringReader(leftRows), new StringReader(rightRows));
        } catch (IOException e) {
            return hilites;
        }

        // what we can hilite in first source
        for (Difference diff : diffs) {
            if (diff.getType() == Difference.ADD) continue;
            int start = rowOffset(leftRows, diff.getFirstStart());
            int end = rowOffset(leftRows, diff.getFirstEnd() + 1);
            
            SimpleAttributeSet attrs = new SimpleAttributeSet();
            StyleConstants.setBackground(attrs, master.getColor(diff));
            hilites.add(new HighLight(rowStart + start, rowStart + end, attrs));
        }
        return hilites;
    }    
    
    private List<HighLight> computeSecondRowHilites(int rowStart, String left, String right) {
        List<HighLight> hilites = new ArrayList<HighLight>(4);
        
        String leftRows = wordsToRows(left);  
        String rightRows = wordsToRows(right);

        DiffProvider diffprovider = Lookup.getDefault().lookup(DiffProvider.class);
        if (diffprovider == null) {
            return hilites;
        }

        Difference[] diffs;
        try {
            diffs = diffprovider.computeDiff(new StringReader(leftRows), new StringReader(rightRows));
        } catch (IOException e) {
            return hilites;
        }

        // what we can hilite in second source
        for (Difference diff : diffs) {
            if (diff.getType() == Difference.DELETE) continue;
            int start = rowOffset(rightRows, diff.getSecondStart());
            int end = rowOffset(rightRows, diff.getSecondEnd() + 1);
            
            SimpleAttributeSet attrs = new SimpleAttributeSet();
            StyleConstants.setBackground(attrs, master.getColor(diff));
            hilites.add(new HighLight(rowStart + start, rowStart + end, attrs));
        }
        return hilites;
    }

    /**
     * 1-based row index.
     * 
     * @param row
     * @param rowIndex
     * @return
     */
    private int rowOffset(String row, int rowIndex) {
        if (rowIndex == 1) return 0; 
        int newLines = 0;
        for (int i = 0; i < row.length(); i++) {
            char c = row.charAt(i);
            if (c == '\n') {
                newLines++;
                if (--rowIndex == 1) {
                    return i + 1 - newLines;
                }
            }
        }
        return row.length();
    }

    private String wordsToRows(String s) {
        StringBuilder sb = new StringBuilder(s.length() * 2);
        StringTokenizer st = new StringTokenizer(s, " \t\n[]{};:'\",.<>/?-=_+\\|~!@#$%^&*()", true); // NOI18N
        while (st.hasMoreTokens()) {
            String token = st.nextToken();
            if (token.length() == 0) continue;
            sb.append(token);
            sb.append('\n');
        }
        return sb.toString();
    }

    private boolean isOneLineChange(Difference diff) {
        return diff.getType() == Difference.CHANGE && 
                diff.getFirstEnd() - diff.getFirstStart() == diff.getSecondEnd() - diff.getSecondStart();
    }

    private void computeDecorations() {
        
        Document document = master.getEditorPane2().getEditorPane().getDocument();
        EditorUI editorUI = org.netbeans.editor.Utilities.getEditorUI(rightContentPanel.getEditorPane());
        if (editorUI == null) return;
        int lineHeight = editorUI.getLineHeight();
        
        Difference [] diffs = master.getDifferences();
        decorationsCached = new DecoratedDifference[diffs.length];
        for (int i = 0; i < diffs.length; i++) {
            Difference difference = diffs[i];
            DecoratedDifference dd = new DecoratedDifference(difference, canRollback(document, difference));
            
            if (difference.getType() == Difference.ADD) {
                dd.topRight = (difference.getSecondStart() - 1) * lineHeight;
                dd.bottomRight = difference.getSecondEnd() * lineHeight;
                dd.topLeft = difference.getFirstStart() * lineHeight;
                dd.floodFill = true;
            } else if (difference.getType() == Difference.DELETE) {
                dd.topLeft = (difference.getFirstStart() - 1) * lineHeight;
                dd.bottomLeft = difference.getFirstEnd() * lineHeight;
                dd.topRight = difference.getSecondStart() * lineHeight;
                dd.floodFill = true;
            } else {
                dd.topRight = (difference.getSecondStart() - 1) * lineHeight;
                dd.bottomRight = difference.getSecondEnd() * lineHeight;
                dd.topLeft = (difference.getFirstStart() - 1) * lineHeight;
                dd.bottomLeft = difference.getFirstEnd() * lineHeight;
                dd.floodFill = true;
            }
            decorationsCached[i] = dd;
        }
    }

    private boolean canRollback(Document doc, Difference diff) {
        if (!(doc instanceof GuardedDocument)) return true;
        GuardedDocument document = (GuardedDocument) doc;
        int start, end;
        if (diff.getType() == Difference.DELETE) {
            start = end = Utilities.getRowStartFromLineOffset(document, diff.getSecondStart());
        } else {
            start = Utilities.getRowStartFromLineOffset(document, diff.getSecondStart() - 1);
            end = Utilities.getRowStartFromLineOffset(document, diff.getSecondEnd());
        }
        MarkBlockChain mbc = ((GuardedDocument) document).getGuardedBlockChain();
        return (mbc.compareBlock(start, end) & MarkBlock.OVERLAP) == 0;
    }
    
    /**
     * 1. find the difference whose top (first line) is closest to the center of the screen. If there is no difference on screen, proceed to #5
     * 2. find line offset of the found difference in the other document
     * 3. scroll the other document so that the difference starts on the same visual line
     * 
     * 5. scroll the other document proportionally
     */ 
    private void smartScroll() {
        DiffContentPanel rightPane = master.getEditorPane2();
        DiffContentPanel leftPane = master.getEditorPane1();        
        
        int [] map = scrollMap.getScrollMap(rightPane.getEditorPane().getSize().height, master.getDiffSerial());
        
        int rightOffet = rightPane.getScrollPane().getVerticalScrollBar().getValue();
        if (rightOffet >= map.length) return;
        leftPane.getScrollPane().getVerticalScrollBar().setValue(map[rightOffet]);
    }

    private int computeLeftOffsetToMatchDifference(DifferencePosition differenceMatchStart, int lineHeight, int rightOffset) {

        Difference diff = differenceMatchStart.getDiff();
        boolean matchStart = differenceMatchStart.isStart();
        
        int value;
        int valueSecond;
        if (matchStart) {
            value = diff.getFirstStart() * lineHeight;        // kde zacina prva, 180
            valueSecond = diff.getSecondStart() * lineHeight; // kde by zacinala druha, napr. 230
        } else {
            if (diff.getType() == Difference.ADD) {
                value = diff.getFirstStart() * lineHeight;        // kde zacina prva, 180
                value -= lineHeight;
                valueSecond = diff.getSecondEnd() * lineHeight; // kde by zacinala druha, napr. 230
            } else {
                value = diff.getFirstEnd() * lineHeight;        // kde zacina prva, 180
                if (diff.getType() == Difference.DELETE) {
                    value += lineHeight;
                    valueSecond = diff.getSecondStart() * lineHeight; // kde by zacinala druha, napr. 230
                } else {
                    valueSecond = diff.getSecondEnd() * lineHeight; // kde by zacinala druha, napr. 230
                }
            }
        }

        // druha je na 400
        int secondOffset = rightOffset - valueSecond;
        
        value += secondOffset;
        if (diff.getType() == Difference.ADD) value += lineHeight;
        if (diff.getType() == Difference.DELETE) value -= lineHeight;
        
        return value;
    }
    
    private DifferencePosition findDifferenceToMatch(int rightOffset, int rightViewportHeight) {
        
        DecoratedDifference candidate = null;
        
        DecoratedDifference [] diffs = getDecorations();
        for (DecoratedDifference dd : diffs) {
            if (dd.getTopRight() > rightOffset + rightViewportHeight) break;
            if (dd.getBottomRight() != -1) {
                if (dd.getBottomRight() <= rightOffset) continue;
            } else {
                if (dd.getTopRight() <= rightOffset) continue;
            }
            if (candidate != null) {
                if (candidate.getDiff().getType() == Difference.DELETE) {
                    candidate = dd;
                } else if (candidate.getTopRight() < rightOffset) { 
                    candidate = dd;
                } else if (dd.getTopRight() <= rightOffset + rightViewportHeight / 2) { 
                    candidate = dd;
                }
            } else {
                candidate = dd;
            }
        }
        if (candidate == null) return null;
        boolean matchStart = candidate.getTopRight() > rightOffset + rightViewportHeight / 2;
        if (candidate.getDiff().getType() == Difference.DELETE && candidate.getTopRight() < rightOffset + rightViewportHeight * 4 / 5) matchStart = false;
        if (candidate.getDiff().getType() == Difference.DELETE && candidate == diffs[diffs.length -1]) matchStart = false;
        return new DifferencePosition(candidate.getDiff(), matchStart);
    }

    double getScrollFactor() {
        BoundedRangeModel m1 = leftContentPanel.getScrollPane().getVerticalScrollBar().getModel();
        BoundedRangeModel m2 = rightContentPanel.getScrollPane().getVerticalScrollBar().getModel();
        return ((double) m1.getMaximum() - m1.getExtent()) / (m2.getMaximum() - m2.getExtent());
    }


    /**
     * The split pane needs to be repainted along with editor.
     * 
     * @param decoratedEditorPane the pane that is currently repainting
     */ 
    void editorPainting(DecoratedEditorPane decoratedEditorPane) {
        if (!decoratedEditorPane.isFirst()) {
            JComponent mydivider = master.getMyDivider();
            mydivider.paint(mydivider.getGraphics());
        }
    }
    
    public static class DifferencePosition {
        
        private Difference  diff;
        private boolean     isStart;

        public DifferencePosition(Difference diff, boolean start) {
            this.diff = diff;
            isStart = start;
        }

        public Difference getDiff() {
            return diff;
        }

        public boolean isStart() {
            return isStart;
        }
    }    

    public static class DecoratedDifference {
        private final Difference    diff;
        private final boolean       canRollback;
        private int         topLeft;            // top line in the left pane
        private int         bottomLeft = -1;    // bottom line in the left pane, -1 for ADDs
        private int         topRight;
        private int         bottomRight = -1;   // bottom line in the right pane, -1 for DELETEs
        private boolean     floodFill;          // should the whole difference be highlited

        public DecoratedDifference(Difference difference, boolean canRollback) {
            diff = difference;
            this.canRollback = canRollback;
        }

        public boolean canRollback() {
            return canRollback;
        }

        public Difference getDiff() {
            return diff;
        }

        public int getTopLeft() {
            return topLeft;
        }

        public int getBottomLeft() {
            return bottomLeft;
        }

        public int getTopRight() {
            return topRight;
        }

        public int getBottomRight() {
            return bottomRight;
        }

        public boolean isFloodFill() {
            return floodFill;
        }
    }

    public static class HighLight {
        
        private final int           startOffset;
        private final int           endOffset;
        private final AttributeSet  attrs;

        public HighLight(int startOffset, int endOffset, AttributeSet attrs) {
            this.startOffset = startOffset;
            this.endOffset = endOffset;
            this.attrs = attrs;
        }

        public int getStartOffset() {
            return startOffset;
        }

        public int getEndOffset() {
            return endOffset;
        }

        public AttributeSet getAttrs() {
            return attrs;
        }
    }

    /**
     * Java StringTokenizer does not work if the very first character is a delimiter.
     */
    private static class CorrectRowTokenizer {
        
        private final String s;
        private int idx;

        public CorrectRowTokenizer(String s) {
            this.s = s;
        }

        public String nextToken() {
            String token = null;
            for (int end = idx; end < s.length(); end++) {
                if (s.charAt(end) == '\n') {
                    token = s.substring(idx, end);
                    idx = end + 1;
                    break;
                }
            }
            return token;
        }
    }

    private class ScrollMapCached {
        
        private int     rightPanelHeightCached;
        private int []  scrollMapCached;
        private int     diffSerialCached;

        public synchronized int[] getScrollMap(int rightPanelHeight, int diffSerial) {
            if (rightPanelHeight != rightPanelHeightCached || diffSerialCached != diffSerial || scrollMapCached == null) {
                diffSerialCached = diffSerial;
                rightPanelHeightCached = rightPanelHeight;
                scrollMapCached = compute();
            }
            return scrollMapCached;
        }

        private int [] compute() {
            DiffContentPanel rightPane = master.getEditorPane2();

            int rightViewportHeight = rightPane.getScrollPane().getViewport().getViewRect().height; 

            int [] scrollMap = new int[rightPanelHeightCached];

            EditorUI editorUI = org.netbeans.editor.Utilities.getEditorUI(leftContentPanel.getEditorPane());
            if (editorUI == null) return scrollMap;
            int lineHeight = editorUI.getLineHeight();

            int lastOffset = 0;
            for (int rightOffset = 0; rightOffset < rightPanelHeightCached; rightOffset++) {
                DifferencePosition dpos = findDifferenceToMatch(rightOffset, rightViewportHeight);
                int leftOffset;
                if (dpos == null) {
                    leftOffset = lastOffset + rightOffset;
                } else {
                    leftOffset = computeLeftOffsetToMatchDifference(dpos, lineHeight, rightOffset);
                    lastOffset = leftOffset - rightOffset;
                }
                scrollMap[rightOffset] = leftOffset;
            }
            scrollMap = smooth(scrollMap);
            return scrollMap;
        }

        private int[] smooth(int[] map) {
            int [] newMap = new int [map.length];
            int leftShift = 0;
            float correction = 0.0f;
            for (int i = 0; i < map.length; i++) {
                int leftOffset = map[i];
                int requestedShift = leftOffset - i; 
                if (requestedShift > leftShift) {
                    if (correction > requestedShift - leftShift) correction = requestedShift - leftShift;
                    leftShift += correction;
                    correction += 0.02f;
                } else if (requestedShift < leftShift) {
                    leftShift -= 1;
                } else {
                    correction = 1.0f;
                }
                newMap[i] = i + leftShift;
            }
            return newMap;
        }
    }        
}
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.