Package com.jediterm.swing

Source Code of com.jediterm.swing.SwingTerminalPanel$PendingScrolls

/* -*-mode:java; c-basic-offset:2; -*- */
/* JCTerm
* Copyright (C) 2002-2004 ymnk, JCraft,Inc.
* Written by: 2002 ymnk<ymnk@jcaft.com>
*  
*  
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public License
* as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

package com.jediterm.swing;

import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.IOException;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import com.jediterm.*;
import org.apache.log4j.Logger;

import com.jediterm.TextStyle;

public class SwingTerminalPanel extends JComponent implements TerminalDisplay, ClipboardOwner, StyledTextConsumer {
  private static final Logger logger = Logger.getLogger(SwingTerminalPanel.class);
  private static final long serialVersionUID = -1048763516632093014L;
  private static final double FPS = 50;

  private BufferedImage myImage;

  private Graphics2D myGfx;

  private final Component myTerminalPanel = this;

  private Font myNormalFont;

  private Font myBoldFont;

  private int myDescent = 0;

  private int myLineSpace = 0;

  Dimension myCharSize = new Dimension();

  Dimension myTermSize = new Dimension(80, 24);

  private boolean myAntialiasing = true;

  private Emulator myEmulator = null;

  protected Point mySelectionStart;

  protected Point mySelectionEnd;

  protected boolean mySelectionInProgress;

  private Clipboard myClipboard;

  private ResizePanelDelegate myResizePanelDelegate;

  final private BackBuffer myBackBuffer;
  final private LinesBuffer myScrollBuffer;
  final private StyleState myStyleState;

  final private Cursor myCursor = new Cursor();

  private final BoundedRangeModel myBoundedRangeModel = new DefaultBoundedRangeModel(0, 80, 0, 80);

  protected int myClientScrollOrigin;
  protected int newClientScrollOrigin;
  private KeyListener myKeyListener;
  private long myLastCursorChange;
  private boolean myCursorIsShown;
  private long myLastResize;


  public SwingTerminalPanel(BackBuffer backBuffer, LinesBuffer scrollBuffer, StyleState styleState) {
    myScrollBuffer = scrollBuffer;
    myBackBuffer = backBuffer;
    myStyleState = styleState;
    myBoundedRangeModel.setRangeProperties(0, myTermSize.height, -scrollBuffer.getLineCount(), myTermSize.height, false);
  }

  public void init() {
    myNormalFont = createFont();
    myBoldFont = myNormalFont.deriveFont(Font.BOLD);

    establishFontMetrics();

    setUpImages();
    setUpClipboard();
    setAntiAliasing(myAntialiasing);

    setPreferredSize(new Dimension(getPixelWidth(), getPixelHeight()));

    setFocusable(true);
    enableInputMethods(true);

    setFocusTraversalKeysEnabled(false);

    addMouseMotionListener(new MouseMotionAdapter() {
      @Override
      public void mouseDragged(final MouseEvent e) {
        final Point charCoords = panelToCharCoords(e.getPoint());

        if (!mySelectionInProgress) {
          mySelectionStart = new Point(charCoords);
          mySelectionInProgress = true;
        }
        repaint();
        mySelectionEnd = charCoords;
        mySelectionEnd.x = Math.min(mySelectionEnd.x + 1, myTermSize.width);
      }
    });

    addMouseWheelListener(new MouseWheelListener() {
      @Override
      public void mouseWheelMoved(MouseWheelEvent e) {
        int notches = e.getWheelRotation();
        myBoundedRangeModel.setValue(myBoundedRangeModel.getValue() + notches);
      }
    });

    addMouseListener(new MouseAdapter() {
      @Override
      public void mouseReleased(final MouseEvent e) {
        mySelectionInProgress = false;

        repaint();
      }

      @Override
      public void mouseClicked(final MouseEvent e) {
        requestFocusInWindow();
        if (e.getButton() == MouseEvent.BUTTON3) {
          JPopupMenu popup = createPopupMenu(mySelectionStart, mySelectionEnd, getClipboardString());
          popup.show(e.getComponent(), e.getX(), e.getY());
        }
        repaint();
      }
    });

    addComponentListener(new ComponentAdapter() {
      @Override
      public void componentResized(final ComponentEvent e) {
        myLastResize = System.currentTimeMillis();
        sizeTerminalFromComponent();
      }
    });

    myBoundedRangeModel.addChangeListener(new ChangeListener() {
      public void stateChanged(final ChangeEvent e) {
        newClientScrollOrigin = myBoundedRangeModel.getValue();
      }
    });

    Timer redrawTimer = new Timer((int)(1000 / FPS), new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        redraw();
      }
    });
    setDoubleBuffered(true);
    redrawTimer.start();
    repaint();
  }

  protected Font createFont() {
    return Font.decode("Monospaced-14");
  }

  private Point panelToCharCoords(final Point p) {
    return new Point(p.x / myCharSize.width, p.y / myCharSize.height + myClientScrollOrigin);
  }

  void setUpClipboard() {
    myClipboard = Toolkit.getDefaultToolkit().getSystemSelection();
    if (myClipboard == null) {
      myClipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
    }
  }

  private void copySelection(final Point selectionStart, final Point selectionEnd) {
    if (selectionStart == null || selectionEnd == null) {
      return;
    }

    Point top;
    Point bottom;

    if (selectionStart.y == selectionEnd.y) {                        /* same line */
      top = selectionStart.x < selectionEnd.x ? selectionStart
                                              : selectionEnd;
      bottom = selectionStart.x >= selectionEnd.x ? selectionStart
                                                  : selectionEnd;
    }
    else {
      top = selectionStart.y < selectionEnd.y ? selectionStart
                                              : selectionEnd;
      bottom = selectionStart.y > selectionEnd.y ? selectionStart
                                                 : selectionEnd;
    }

    final StringBuilder selectionText = new StringBuilder();

    if (top.y < 0) {
      final Point scrollEnd = bottom.y >= 0 ? new Point(myTermSize.width, -1) : bottom;
      myScrollBuffer.processLines(top.y, scrollEnd.y - top.y,
                                  new SelectionTextAppender(selectionText, top, scrollEnd));
    }

    if (bottom.y >= 0) {
      final Point backBegin = top.y < 0 ? new Point(0, 0) : top;
      myBackBuffer.processBufferCells(0, backBegin.y, myTermSize.width, bottom.y - backBegin.y + 1,
                                      new SelectionTextAppender(selectionText, backBegin, bottom));
    }

    if (selectionText.length() != 0) {

      try {
        setCopyContents(new StringSelection(selectionText.toString()));
      }
      catch (final IllegalStateException e) {
        logger.error("Could not set clipboard:", e);
      }
    }
  }

  protected void setCopyContents(StringSelection selection) {
    myClipboard.setContents(selection, this);
  }

  private void pasteSelection() {
    final String selection = getClipboardString();

    try {
      myEmulator.sendString(selection);
    }
    catch (IOException e) {
      logger.info(e);
    }
  }

  private String getClipboardString() {
    try {
      return getClipboardContent();
    }
    catch (final Exception e) {
      logger.info(e);
    }
    return null;
  }

  protected String getClipboardContent() throws IOException, UnsupportedFlavorException {
    try {
      return (String)myClipboard.getData(DataFlavor.stringFlavor);
    }
    catch (Exception e) {
      logger.info(e);
      return null;
    }
  }

  /* Do not care
   */
  public void lostOwnership(final Clipboard clipboard, final Transferable contents) {
  }

  private void setUpImages() {
    final BufferedImage oldImage = myImage;
    int width = getPixelWidth();
    int height = getPixelHeight();
    if (width > 0 && height > 0) {
      myImage = createBufferedImage(width, height);
      myGfx = myImage.createGraphics();

      myGfx.setColor(getBackground());

      myGfx.fillRect(0, 0, width, height);

      if (oldImage != null) {
        myGfx.drawImage(oldImage, 0, 0,
                        oldImage.getWidth(), oldImage.getHeight(), myTerminalPanel);
      }
    }
  }

  protected BufferedImage createBufferedImage(int width, int height) {
    return new BufferedImage(width, height,
                             BufferedImage.TYPE_INT_RGB);
  }

  private void sizeTerminalFromComponent() {
    if (myEmulator != null) {
      final int newWidth = getWidth() / myCharSize.width;
      final int newHeight = getHeight() / myCharSize.height;

      if (newHeight > 0 && newWidth > 0) {
        final Dimension newSize = new Dimension(newWidth, newHeight);

        myEmulator.postResize(newSize, RequestOrigin.User);
      }
    }
  }

  public void setEmulator(final Emulator emulator) {
    myEmulator = emulator;
    sizeTerminalFromComponent();
  }

  public void setKeyListener(final KeyListener keyListener) {
    this.myKeyListener = keyListener;
  }

  public Dimension requestResize(final Dimension newSize,
                                 final RequestOrigin origin,
                                 int cursorY,
                                 BufferedTerminalWriter.ResizeHandler resizeHandler) {
    if (!newSize.equals(myTermSize)) {
      myBackBuffer.lock();
      try {
        myBackBuffer.resize(newSize, origin, cursorY, resizeHandler);
        myTermSize = (Dimension)newSize.clone();
        // resize images..
        setUpImages();

        final Dimension pixelDimension = new Dimension(getPixelWidth(), getPixelHeight());

        setPreferredSize(pixelDimension);
        if (myResizePanelDelegate != null) myResizePanelDelegate.resizedPanel(pixelDimension, origin);
        myBoundedRangeModel.setRangeProperties(0, myTermSize.height, -myScrollBuffer.getLineCount(), myTermSize.height, false);
      }
      finally {
        myBackBuffer.unlock();
      }
    }

    return new Dimension(getPixelWidth(), getPixelHeight());
  }

  public void setResizePanelDelegate(final ResizePanelDelegate resizeDelegate) {
    myResizePanelDelegate = resizeDelegate;
  }

  private void establishFontMetrics() {
    final BufferedImage img = createBufferedImage(1, 1);
    final Graphics2D graphics = img.createGraphics();
    graphics.setFont(myNormalFont);

    final FontMetrics fo = graphics.getFontMetrics();
    myDescent = fo.getDescent();
    myCharSize.width = fo.charWidth('@');
    myCharSize.height = fo.getHeight() + myLineSpace * 2;
    myDescent += myLineSpace;

    img.flush();
    graphics.dispose();
  }

  protected void setupAntialiasing(Graphics graphics, boolean antialiasing) {
    myAntialiasing = antialiasing;
    if (graphics instanceof Graphics2D) {
      Graphics2D myGfx = (Graphics2D)graphics;
      final Object mode = antialiasing ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON
                                       : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
      final RenderingHints hints = new RenderingHints(
        RenderingHints.KEY_TEXT_ANTIALIASING, mode);
      myGfx.setRenderingHints(hints);
    }
  }


  @Override
  public void paint(Graphics g) {
    setupAntialiasing(g, myAntialiasing);

    super.paint(g);
  }

  @Override
  public void paintComponent(final Graphics g) {
    Graphics2D gfx = (Graphics2D)g;
    if (myImage != null) {
      gfx.drawImage(myImage, 0, 0, myTerminalPanel);
      drawMargins(gfx, myImage.getWidth(), myImage.getHeight());
      drawSelection(gfx);
      myCursor.drawCursor(gfx);
    }
  }

  @Override
  public void processKeyEvent(final KeyEvent e) {
    final int id = e.getID();
    if (id == KeyEvent.KEY_PRESSED) {
      myKeyListener.keyPressed(e);
    }
    else if (id == KeyEvent.KEY_RELEASED) {
                        /* keyReleased(e); */
    }
    else if (id == KeyEvent.KEY_TYPED) {
      myKeyListener.keyTyped(e);
    }
    e.consume();
  }


  public int getPixelWidth() {
    return myCharSize.width * myTermSize.width;
  }

  public int getPixelHeight() {
    return myCharSize.height * myTermSize.height;
  }

  public int getColumnCount() {
    return myTermSize.width;
  }

  public int getRowCount() {
    return myTermSize.height;
  }


  private class Cursor {
    private static final long CURSOR_BLINK_PERIOD = 505;


    private boolean myCursorHasChanged;

    protected Point myCursorCoordinates = new Point();
    private boolean myShouldDrawCursor = true;

    private boolean calculateIsCursorShown(long currentTime) {
      if (myCursorHasChanged) {
        return true;
      }
      if (cursorShouldChangeBlinkState(currentTime)) {
        return !myCursorIsShown;
      }
      else {
        return myCursorIsShown;
      }
    }

    private boolean cursorShouldChangeBlinkState(long currentTime) {
      return currentTime - myLastCursorChange > CURSOR_BLINK_PERIOD;
    }

    public void drawCursor(Graphics2D g) {
      if (needsRepaint()) {
        final int y = (myCursorCoordinates.y - 1 - myClientScrollOrigin);

        if (y >= 0 && y < myTermSize.height) {
          TextStyle current = myStyleState.getCurrent();

          boolean isCursorShown = calculateIsCursorShown(System.currentTimeMillis());

          if (isCursorShown) {
            g.setColor(current.getForeground());
          }
          else {
            g.setColor(current.getBackground());
          }
          g.fillRect(myCursorCoordinates.x * myCharSize.width, y * myCharSize.height,
                     myCharSize.width, myCharSize.height);


          myCursorIsShown = isCursorShown;
          myLastCursorChange = System.currentTimeMillis();

          myCursorHasChanged = false;
        }
      }
    }

    public boolean needsRepaint() {
      long currentTime = System.currentTimeMillis();
      return isShouldDrawCursor() &&
             isFocusOwner() &&
             noRecentResize(currentTime) &&
             (myCursorHasChanged || cursorShouldChangeBlinkState(currentTime));
    }

    public void setX(int x) {
      myCursorCoordinates.x = x;
      myCursorHasChanged = true;
    }

    public void setY(int y) {
      myCursorCoordinates.y = y;
      myCursorHasChanged = true;
    }

    public void setShouldDrawCursor(boolean shouldDrawCursor) {
      myShouldDrawCursor = shouldDrawCursor;
    }

    public boolean isShouldDrawCursor() {
      return myShouldDrawCursor;
    }

    private boolean noRecentResize(long time) {
      return time - myLastResize > CURSOR_BLINK_PERIOD;
    }
  }

  public void drawSelection(Graphics2D g) {
                /* which is the top one */
    Point top;
    Point bottom;
    TextStyle current = myStyleState.getCurrent();
    g.setColor(current.getForeground());
    g.setXORMode(current.getBackground());
    if (mySelectionStart == null || mySelectionEnd == null) {
      return;
    }

    if (mySelectionStart.y == mySelectionEnd.y) {
                        /* same line */
      if (mySelectionStart.x == mySelectionEnd.x) {
        return;
      }
      top = mySelectionStart.x < mySelectionEnd.x ? mySelectionStart
                                                  : mySelectionEnd;
      bottom = mySelectionStart.x >= mySelectionEnd.x ? mySelectionStart
                                                      : mySelectionEnd;

      g.fillRect(top.x * myCharSize.width, (top.y - myClientScrollOrigin) * myCharSize.height,
                 (bottom.x - top.x) * myCharSize.width, myCharSize.height);
    }
    else {
      top = mySelectionStart.y < mySelectionEnd.y ? mySelectionStart
                                                  : mySelectionEnd;
      bottom = mySelectionStart.y > mySelectionEnd.y ? mySelectionStart
                                                     : mySelectionEnd;
                        /* to end of first line */
      g.fillRect(top.x * myCharSize.width, (top.y - myClientScrollOrigin) * myCharSize.height,
                 (myTermSize.width - top.x) * myCharSize.width, myCharSize.height);

      if (bottom.y - top.y > 1) {
                                /* intermediate lines */
        g.fillRect(0, (top.y + 1 - myClientScrollOrigin) * myCharSize.height,
                   myTermSize.width * myCharSize.width, (bottom.y - top.y - 1)
                                                        * myCharSize.height);
      }

      /* from beginning of last line */

      g.fillRect(0, (bottom.y - myClientScrollOrigin) * myCharSize.height, bottom.x
                                                                           * myCharSize.width, myCharSize.height);
    }
  }

  @Override
  public void consume(int x, int y, TextStyle style, CharBuffer buf) {
    if (myGfx != null) {
      myGfx.setColor(myStyleState.getBackground(style.getBackgroundForRun()));
      myGfx
        .fillRect(x * myCharSize.width, (y - myClientScrollOrigin) * myCharSize.height, buf.getLen() * myCharSize.width, myCharSize.height);

      myGfx.setFont(style.hasOption(TextStyle.Option.BOLD) ? myBoldFont : myNormalFont);
      myGfx.setColor(myStyleState.getForeground(style.getForegroundForRun()));

      int baseLine = (y + 1 - myClientScrollOrigin) * myCharSize.height - myDescent;


      myGfx.drawChars(buf.getBuf(), buf.getStart(), buf.getLen(), x * myCharSize.width, baseLine);

      if (style.hasOption(TextStyle.Option.UNDERSCORE)) {
        myGfx.drawLine(x * myCharSize.width, baseLine + 1, (x + buf.getLen()) * myCharSize.width, baseLine + 1);
      }
    }
  }

  private void clientScrollOriginChanged(int oldOrigin) {
    int dy = myClientScrollOrigin - oldOrigin;

    int dyPix = dy * myCharSize.height;

    myGfx.copyArea(0, Math.max(0, dyPix),
                   getPixelWidth(), getPixelHeight() - Math.abs(dyPix),
                   0, -dyPix);

    if (dy < 0) {
      // Scrolling up; Copied down
      // New area at the top to be filled in - can only be from scroll buffer
      //

      myScrollBuffer.processLines(myClientScrollOrigin, -dy, this);
    }
    else {
      //Scrolling down; Copied up
      // New area at the bottom to be filled - can be from both

      int oldEnd = oldOrigin + myTermSize.height;

      // Either its the whole amount above the back buffer + some more
      // Or its the whole amount we moved
      // Or we are already out of the scroll buffer
      int portionInScroll = oldEnd < 0 ? Math.min(-oldEnd, dy) : 0;

      int portionInBackBuffer = dy - portionInScroll;

      if (portionInScroll > 0) {
        myScrollBuffer.processLines(oldEnd, portionInScroll, this);
      }

      if (portionInBackBuffer > 0) {
        myBackBuffer.processBufferRows(oldEnd + portionInScroll, portionInBackBuffer, this);
      }
    }
  }

  int myNoDamage = 0;
  int myFramesSkipped = 0;

  public void redraw() {
    if (tryRedrawDamagedPartFromBuffer() || myCursor.needsRepaint()) {
      repaint();
    }
  }

  /**
   * This method tries to get a lock for back buffer. If it fails it increments skippedFrames counter and tries next time.
   * After 5 attempts it locks buffer anyway.
   *
   * @return true if was successfully redrawn and there is anything to repaint
   */
  private boolean tryRedrawDamagedPartFromBuffer() {
    final int newOrigin = newClientScrollOrigin;
    if (!myBackBuffer.tryLock()) {
      if (myFramesSkipped >= 5) {
        myBackBuffer.lock();
      }
      else {
        myFramesSkipped++;
        return false;
      }
    }
    try {
      myFramesSkipped = 0;

      boolean serverScroll = pendingScrolls.enact(myGfx, getPixelWidth(), myCharSize.height);

      boolean clientScroll = myClientScrollOrigin != newOrigin;
      if (clientScroll) {
        final int oldOrigin = myClientScrollOrigin;
        myClientScrollOrigin = newOrigin;
        clientScrollOriginChanged(oldOrigin);
      }

      boolean hasDamage = myBackBuffer.hasDamage();
      if (hasDamage) {
        myNoDamage = 0;

        myBackBuffer.processDamagedCells(this);
        myBackBuffer.resetDamage();
      }
      else {
        myNoDamage++;
      }

      return serverScroll || clientScroll || hasDamage;
    }
    finally {
      myBackBuffer.unlock();
    }
  }

  private void drawMargins(Graphics2D gfx, int width, int height) {
    gfx.setColor(getBackground());
    gfx.fillRect(0, height, getWidth(), getHeight() - height);
    gfx.fillRect(width, 0, getWidth() - width, getHeight());
  }

  public void scrollArea(final int y, final int h, int dy) {
    if (dy < 0) {
      //Moving lines off the top of the screen
      //TODO: Something to do with application keypad mode
      //TODO: Something to do with the scroll margins

      SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
          myBoundedRangeModel.setRangeProperties(0, myTermSize.height, -myScrollBuffer.getLineCount(), myTermSize.height, false);
        }
      });
    }
    mySelectionStart = null;
    mySelectionEnd = null;
    pendingScrolls.add(y, h, dy);
  }

  private static class PendingScrolls {
    int[] ys = new int[10];
    int[] hs = new int[10];
    int[] dys = new int[10];
    int scrollCount = -1;

    void ensureArrays(int index) {
      int curLen = ys.length;
      if (index >= curLen) {
        ys = Util.copyOf(ys, curLen * 2);
        hs = Util.copyOf(hs, curLen * 2);
        dys = Util.copyOf(dys, curLen * 2);
      }
    }

    void add(int y, int h, int dy) {
      if (dy == 0) return;
      if (scrollCount >= 0 &&
          y == ys[scrollCount] &&
          h == hs[scrollCount]) {
        dys[scrollCount] += dy;
      }
      else {
        scrollCount++;
        ensureArrays(scrollCount);
        ys[scrollCount] = y;
        hs[scrollCount] = h;
        dys[scrollCount] = dy;
      }
    }

    boolean enact(Graphics2D gfx, int width, int charHeight) {
      if (scrollCount < 0) return false;
      for (int i = 0; i <= scrollCount; i++) {
        gfx.copyArea(0, ys[i] * charHeight, width, hs[i] * charHeight, 0, dys[i] * charHeight);
      }
      scrollCount = -1;
      return true;
    }
  }

  final PendingScrolls pendingScrolls = new PendingScrolls();

  public void setCursor(final int x, final int y) {
    myCursor.setX(x);
    myCursor.setY(y);
  }

  public void beep() {
    Toolkit.getDefaultToolkit().beep();
  }

  public void setLineSpace(final int foo) {
    myLineSpace = foo;
  }

  public void setAntiAliasing(final boolean antialiasing) {
    if (myGfx == null) {
      return;
    }
    setupAntialiasing(myGfx, antialiasing);
  }

  public BoundedRangeModel getBoundedRangeModel() {
    return myBoundedRangeModel;
  }

  public BackBuffer getBackBuffer() {
    return myBackBuffer;
  }

  public LinesBuffer getScrollBuffer() {
    return myScrollBuffer;
  }

  public void lock() {
    myBackBuffer.lock();
  }

  public void unlock() {
    myBackBuffer.unlock();
  }

  @Override
  public void setShouldDrawCursor(boolean shouldDrawCursor) {
    myCursor.setShouldDrawCursor(shouldDrawCursor);
  }


  protected JPopupMenu createPopupMenu(final Point selectionStart, final Point selectionEnd, String content) {
    JPopupMenu popup = new JPopupMenu();

    ActionListener popupListener = new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        if ("Copy".equals(e.getActionCommand())) {
          copySelection(selectionStart, selectionEnd);
        }
        else if ("Paste".equals(e.getActionCommand())) {
          pasteSelection();
        }
      }
    };

    JMenuItem menuItem = new JMenuItem("Copy");
    menuItem.addActionListener(popupListener);
    menuItem.setEnabled(selectionStart != null);
    popup.add(menuItem);
    menuItem = new JMenuItem("Paste");
    menuItem.setEnabled(content != null);
    menuItem.addActionListener(popupListener);
    popup.add(menuItem);

    return popup;
  }
}
TOP

Related Classes of com.jediterm.swing.SwingTerminalPanel$PendingScrolls

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.