Easy to Learn Java: Programming Articles, Examples and Tips

Start with Java in a few days with Java Lessons or Lectures

Home

Code Examples

Java Tools

More Java Tools!

Java Forum

All Java Tips

Books

Submit News
Search the site here...
Search...
 

Swing Chapter 15. (Advanced topics) Layered Panes and custom MDI. Easy for reading, Click here!

Custom Search
Swing Chapter 15. (Advanced topics) Layered Panes and custom MDI. Easy for reading, Click here!

[ Return to Swing (Book) ]

Page: 5/7 



Previous Page Previous Page (4/7) - Next Page (6/7) Next Page
Subpages: 1. JLayeredPane 
2.
Using JLayeredPane to enhance interfaces 
3.
Creating a custom MDI: part I - Dragging panels 
4. Creating a custom MDI: part II - Resizability 
5. Creating a custom MDI: part III - Enhancements 
6. Creating a custom MDI: part IV - Selection and management 
7. Creating a custom MDI: part V - JavaBeans compliance 

15.5  Creating a custom MDI: part III - Enhancements

There are a few things about the previous example to take special note of at this stage. You may have noticed that mouse events are still propagating right through our frames. We can also still drag InnerFrames completely outside the layered pane view. In  this section we address these issues, implement maximizable functionality, and take the final step in making InnerFrame a fundamental Swing container by implementing the RootPaneContainer interface.

Figure 15.8. Custom MDI: part III

<<file figure15-8.gif>>

The Code: InnerFrame.java

see \Chapter15\4\mdi

package mdi;

import java.awt.*;

import java.awt.event.*;

import javax.swing.*;

import javax.swing.event.*;

import javax.swing.border.EmptyBorder;

public class InnerFrame

extends JPanel implements RootPaneContainer

{

  private static String IMAGE_DIR = "mdi" + java.io.File.separator;

  private static ImageIcon ICONIZE_BUTTON_ICON =

    new ImageIcon(IMAGE_DIR+"iconize.gif");

  private static ImageIcon RESTORE_BUTTON_ICON =

    new ImageIcon(IMAGE_DIR+"restore.gif");

  private static ImageIcon CLOSE_BUTTON_ICON =

    new ImageIcon(IMAGE_DIR+"close.gif");

  private static ImageIcon MAXIMIZE_BUTTON_ICON =

    new ImageIcon(IMAGE_DIR+"maximize.gif");

  private static ImageIcon MINIMIZE_BUTTON_ICON =

    new ImageIcon(IMAGE_DIR+"minimize.gif");

  private static ImageIcon PRESS_CLOSE_BUTTON_ICON =

    new ImageIcon(IMAGE_DIR+"pressclose.gif");

  private static ImageIcon PRESS_RESTORE_BUTTON_ICON =

    new ImageIcon(IMAGE_DIR+"pressrestore.gif");

  private static ImageIcon PRESS_ICONIZE_BUTTON_ICON =

    new ImageIcon(IMAGE_DIR+"pressiconize.gif");

  private static ImageIcon PRESS_MAXIMIZE_BUTTON_ICON =

    new ImageIcon(IMAGE_DIR+"pressmaximize.gif");

  private static ImageIcon PRESS_MINIMIZE_BUTTON_ICON =

    new ImageIcon(IMAGE_DIR+"pressminimize.gif");

  private static ImageIcon DEFAULT_FRAME_ICON =

    new ImageIcon(IMAGE_DIR+"default.gif");

  private static int BORDER_THICKNESS = 4;

  private static int WIDTH = 200;

  private static int HEIGHT = 200;

  private static int TITLE_BAR_HEIGHT = 25;

  private static int FRAME_ICON_PADDING = 2;

  private static int ICONIZED_WIDTH = 150;

  private static Color TITLE_BAR_BG_COLOR =

    new Color(108,190,116);

  private static Color BORDER_COLOR = new Color(8,90,16);

  private int m_titleBarHeight = TITLE_BAR_HEIGHT;

  private int m_width = WIDTH;

  private int m_height = HEIGHT;

  private int m_iconizedWidth = ICONIZED_WIDTH;

  private int m_x;

  private int m_y;

  private String m_title;

  private JLabel m_titleLabel;

  private JLabel m_iconLabel;

  private boolean m_iconified;

  private boolean m_maximized;

  private boolean m_iconizeable;

  private boolean m_resizeable;

  private boolean m_closeable;

  private boolean m_maximizeable;

  // only false when maximized

  private boolean m_draggable = true;

  private JRootPane m_rootPane;

  // used to wrap m_titlePanel and m_rootPane

  private JPanel m_frameContentPanel;

  private JPanel m_titlePanel;

  private JPanel m_contentPanel;

  private JPanel m_buttonPanel;

  private JPanel m_buttonWrapperPanel;

  private InnerFrameButton m_iconize;

  private InnerFrameButton m_close;

  private InnerFrameButton m_maximize;

  // Unchanged code

  public InnerFrame(String title, ImageIcon frameIcon) {

    this(title, frameIcon, true, true, true, true);

  }

  public InnerFrame(String title, ImageIcon frameIcon,

   boolean resizeable, boolean iconizeable,

   boolean maximizeable, boolean closeable) {

    super.setLayout(new BorderLayout());

    attachNorthResizeEdge();

    attachSouthResizeEdge();

    attachEastResizeEdge();

    attachWestResizeEdge();

    populateInnerFrame();

    setTitle(title);

    setResizeable(resizeable);

    setIconizeable(iconizeable);

    setCloseable(closeable);

    setMaximizeable(maximizeable);

    if (frameIcon != null)

      setFrameIcon(frameIcon);

  }

  protected void populateInnerFrame() {

    m_rootPane = new JRootPane();

    m_frameContentPanel = new JPanel();

    m_frameContentPanel.setLayout(new BorderLayout());

    createTitleBar();

    m_contentPanel = new JPanel(new BorderLayout());

    m_rootPane.setContentPane(m_contentPanel);

    m_frameContentPanel.add(m_titlePanel, BorderLayout.NORTH);

    m_frameContentPanel.add(m_rootPane, BorderLayout.CENTER);

    setupCapturePanel();

    super.add(m_frameContentPanel, BorderLayout.CENTER);

  }

  protected void setupCapturePanel() {

    CapturePanel mouseTrap = new CapturePanel();

    m_rootPane.getLayeredPane().add(mouseTrap,

      new Integer(Integer.MIN_VALUE));

    mouseTrap.setBounds(0,0,10000,10000);

  }

  // don't allow this in root pane containers

  public Component add(Component c) {

    return null;

  }

  // don't allow this in root pane containers

  public void setLayout(LayoutManager mgr) {

  }

  public JMenuBar getJMenuBar() {

    return m_rootPane.getJMenuBar();

  }

  public JRootPane getRootPane() {

    return m_rootPane;

  }

  public Container getContentPane() {

    return m_rootPane.getContentPane();

  }

  public Component getGlassPane() {

    return m_rootPane.getGlassPane();

  }

  public JLayeredPane getLayeredPane() {

    return m_rootPane.getLayeredPane();

  }

  public void setJMenuBar(JMenuBar menu) {

    m_rootPane.setJMenuBar(menu);

  }

  public void setContentPane(Container content) {

    m_rootPane.setContentPane(content);

  }

  public void setGlassPane(Component glass) {

    m_rootPane.setGlassPane(glass);

  }

  public void setLayeredPane(JLayeredPane layered) {

    m_rootPane.setLayeredPane(layered);

  }

  // Unchanged code

  public boolean isMaximizeable() {

    return m_maximizeable;

  }

  public void setMaximizeable(boolean b) {

    m_maximizeable = b;

    m_maximize.setVisible(b);

    m_titlePanel.revalidate();

  }

  public boolean isIconified() {

    return m_iconified;

  }

  public void setIconified(boolean b) {

    m_iconified = b;

    if (b) {

      if (isMaximized())

        setMaximized(false);

      toFront();

      m_width = getWidth();     // remember width

      m_height = getHeight();   // remember height

      setBounds(getX(), getY(), ICONIZED_WIDTH,

        m_titleBarHeight + 2*BORDER_THICKNESS);

      m_iconize.setIcon(RESTORE_BUTTON_ICON);

      m_iconize.setPressedIcon(PRESS_RESTORE_BUTTON_ICON);

      setResizeable(false);

    }

    else {

      toFront();

      setBounds(getX(), getY(), m_width, m_height);

      m_iconize.setIcon(ICONIZE_BUTTON_ICON);

      m_iconize.setPressedIcon(PRESS_ICONIZE_BUTTON_ICON);

      setResizeable(true);

    }

    revalidate();

  }

  public boolean isMaximized() {

    return m_maximized;

  }

  public void setMaximized(boolean b) {

    m_maximized = b;

    if (b)

    {

      if (isIconified())

        setIconified(false);

      toFront();

      m_width = getWidth();     // remember width

      m_height = getHeight();   // remember height

      m_x = getX();             // remember x

      m_y = getY();             // remember y

      setBounds(0, 0, getParent().getWidth(),

        getParent().getHeight());

      m_maximize.setIcon(MINIMIZE_BUTTON_ICON);

      m_maximize.setPressedIcon(PRESS_MINIMIZE_BUTTON_ICON);

      setResizeable(false);

      setDraggable(false);

    }

    else {

      toFront();

      setBounds(m_x, m_y, m_width, m_height);

      m_maximize.setIcon(MAXIMIZE_BUTTON_ICON);

      m_maximize.setPressedIcon(PRESS_MAXIMIZE_BUTTON_ICON);

      setResizeable(true);

      setDraggable(true);

    }

    revalidate();

  }

  // Unchanged code

  public boolean isDraggable() {

    return m_draggable;

  }

  private void setDraggable(boolean b) {

    m_draggable = b;

  }

  // create the title bar: m_titlePanel

  protected void createTitleBar() {

    // Unchanged code

    m_maximize = new InnerFrameButton(MAXIMIZE_BUTTON_ICON);

    m_maximize.setPressedIcon(PRESS_MAXIMIZE_BUTTON_ICON);

    m_maximize.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {

        InnerFrame.this.setMaximized(

          !InnerFrame.this.isMaximized());

      }

    });

    m_buttonWrapperPanel = new JPanel();

    m_buttonWrapperPanel.setOpaque(false);

    m_buttonPanel = new JPanel(new GridLayout(1,3));

    m_buttonPanel.setOpaque(false);

    m_buttonPanel.add(m_iconize);

    m_buttonPanel.add(m_maximize);

    // Unchanged code

  }

  // title bar mouse adapter for frame dragging

  class InnerFrameTitleBarMouseAdapter

  extends MouseInputAdapter

  {

    // Unchanged code

    // don't allow dragging outside of parent

    public void mouseDragged(MouseEvent e) {

      int ex = e.getX();

      int ey = e.getY();

      int x = m_if.getX();

      int y = m_if.getY();

      int w = m_if.getParent().getWidth();

      int h = m_if.getParent().getHeight();

      if (m_dragging & m_if.isDraggable()) {

        if((ey + y > 0 && ey + y < h) &&

          (ex + x > 0 && ex + x < w))

        {

          m_if.setLocation(ex-m_XDifference+x, ey-m_YDifference+y);

        }

        else if (!(ey + y > 0 && ey + y < h) &&

          (ex + x > 0 && ex + x < w))

        {

          if (!(ey + y > 0) && ey + y < h) {

            m_if.setLocation(ex-m_XDifference+x, 0-m_YDifference);

          else if (ey + y > 0 && !(ey + y < h))

            m_if.setLocation(ex-m_XDifference+x, h-m_YDifference);

        }

        else if ((ey + y > 0 && ey + y < h) &&

          !(ex + x > 0 && ex + x < w))

        {

          if (!(ex + x > 0) && ex + x < w)

            m_if.setLocation(0-m_XDifference, ey-m_YDifference+y);

          else if (ex + x > 0 && !(ex + x < w))

            m_if.setLocation(w-m_XDifference, ey-m_YDifference+y);

        }

        else if (!(ey + y > 0) && ey + y < h

         && !(ex + x > 0) && ex + x < w)

          m_if.setLocation(0-m_XDifference, 0-m_YDifference);

        else if (!(ey + y > 0) && ey + y < h

         && ex + x > 0 && !(ex + x < w))

          m_if.setLocation(w-m_XDifference, 0-m_YDifference);

        else if (ey + y > 0 && !(ey + y < h)

         && !(ex + x > 0) && ex + x < w)

          m_if.setLocation(0-m_XDifference, h-m_YDifference);

        else if (ey + y > 0 && !(ey + y < h)

         && ex + x > 0 && !(ex + x < w))

          m_if.setLocation(w-m_XDifference, h-m_YDifference);

      }

    }

    // Unchanged code

  }

  // Unchanged code

  ///////////////////////////////////////////////

  /////////// Mouse Event Capturing /////////////

  ///////////////////////////////////////////////

  class CapturePanel extends JPanel

  {

    public CapturePanel() {

      MouseInputAdapter mia = new MouseInputAdapter() {};

      addMouseListener(mia);

      addMouseMotionListener(mia);

    }

  }

  // Unchanged code

}

Understanding The Code:

Class InnerFrame

New class variables:

ImageIcon MAXIMIZE_BUTTON_ICON: icon used for the maximize button =  [black interior]

ImageIcon PRESS_MAXIMIZE_BUTTON_ICON: icon used for the maximize button in the pressed state =  [dark green interior]

ImageIcon MINIMIZE_BUTTON_ICON: icon used for the maximize button to represent minimization =  [black interior]

ImageIcon PRESS_MINIMIZE_BUTTON_ICON: icon used for the maximize button in the pressed state, representing minimization =  [dark green interior]

New instance variables:

int m_x: used to record the location of InnerFrame before a maximize occurs

int m_y: used to record the location of InnerFrame before a maximize occurs

boolean m_maximizable: true when frame can be maximized.

boolean m_resizable: True when InnerFrame is not iconified or maximized.

JRootPane m_rootPane: central InnerFrame container--all external access is restricted to this container and its panes.

InnerFrameButton m_maximize: the maximize title bar button.

The InnerFrame constructors now support a fourth boolean parameter specifying whether the frame will be maximizable or not.

The populateInnerFrame() method is now responsible for creating a JRootPane to be used as InnerFrame's central container. Since InnerFrame now implements the RootPaneContainer interface, we are required to implement access to this JRootPane and its contentPane, layeredPane, glassPane and JMenuBar just as a JFrame or JInternalFrame. Thus get() and set() methods have been implemented for each of these constituents.

The setupCapturePanel() method places an instance of our mouse-event-consuming panel, CapturePanel (see below),  in the lowest possible layer of of our rootPane's layeredPane:

  protected void setupCapturePanel() {

    CapturePanel mouseTrap = new CapturePanel();

    m_rootPane.getLayeredPane().add(mouseTrap,

      new Integer(Integer.MIN_VALUE));

    mouseTrap.setBounds(0,0,10000,10000);

  }

We set the bounds of this CapturePanel to be extremely large so we are, for all practical purposes, guaranteed that mouse events will not pass through the 'back' of InnerFrame.

The add() and setLayout() methods we had redirected to another panel in the last section, have been modified to return null and do nothing respectively. This enforces InnerFrame container access through its JRootPane constituents, similar to all other primary Swing containers.

The setMaximizable() method has been added to control the state of the m_maximizeable property and the visibility of the m_maximize button in the title bar.

Method setMaximized() has also been added for maximize and minimize functionality. When InnerFrame is told to maximize it first checks to see if it is iconified. If it is it deiconifies itself. Then it records its dimensions and location and resizes itself to be the size of its parent container. It swaps the maximize button icon for that representing minimize, and setResizable(false) is called to remove mouse listeners (we should not be able to resize a maximized frame). Finally a new method called setDraggable() is called and passed a false value. This method controls a flag that the title bar's mouseDragged() method checks before along InnerFrame to be dragged. If we set this flag to false InnerFrame will not be draggable. In the maximized state this is desirable.

  public void setMaximized(boolean b) {

    m_maximized = b;

    if (b)

    {

      if (isIconified())

        setIconified(false);

      toFront();

      m_width = getWidth();     // remember width

      m_height = getHeight();   // remember height

      m_x = getX();             // remember x

      m_y = getY();             // remember y

      setBounds(0, 0, getParent().getWidth(),

        getParent().getHeight());

      m_maximize.setIcon(MINIMIZE_BUTTON_ICON);

      m_maximize.setPressedIcon(PRESS_MINIMIZE_BUTTON_ICON);

      setResizeable(false);

      setDraggable(false);

    }

When a minimize occurs, InnerFrame is moved to the recorded location, set to its stored width and height, and the maximize/minimize button icons are swapped again. setResizable(true) and setDraggable(true) restore full resizeable and draggable functionality:

    else {

      toFront();

      setBounds(m_x, m_y, m_width, m_height);

      m_maximize.setIcon(MAXIMIZE_BUTTON_ICON);

      m_maximize.setPressedIcon(PRESS_MAXIMIZE_BUTTON_ICON);

      setResizeable(true);

      setDraggable(true);

    }

The setIconified() method has been modified to take into account the possibility that InnerFrame may be iconified from within the maximized state. In this case we call setMaximized(false) before proceeding with the iconfication.

The m_maximize button is created for placement in the title bar, and an ActionListener is attached with an actionPerformed() method that invokes setMaximized(). The title bar's button panel then allocates an additional cell (it uses GridLayout) for m_maximize, and it is added between the iconify and close buttons.

Class InnerFrame.InnerFrameTitleBarMouseAdapter

This class's mouseDragged() method is now much more involved. It is somewhat overwhelming at first, but all of this code is actually necessary to smoothly stop the selected InnerFrame from being dragged outside of the visible region of its parent. This code handles all mouse positions allowing vertical movement when horizontal is not possible, horizontal movement when vertical is not possible, and all combinations of possible dragging, while making sure that InnerFrame never leaves the visible region of its parent. It is not necessary that you work through the details, but it is encouraged (similar to the code for the XXResizeEdge classes), as it will provide an appreciation for how complicated situations such as this can be dealt with in an organized mannar.

Reference: Similar code will be used in Chapter 16, section 16.5, where we build an X windows style pager that is not allowed to leave the JDesktopPane view.

Note: We have not implemented code to stop the user from resizing an InnerFrame so that its title bar lies outside of the layered pane view. This can result in a lost frame. In order to provide a solution to this we would have to add a considerable amount of code to the NorthResizeEdge mouseDragged() method. It can be done but we will avoid it here because other issues deserve more attention. In a commercial implementation we would want to include code to watch for this. It is interesting that this is not handled in the JDesktopPane/JInternalFrame MDI.

Class InnerFrame.CapturePanel

As we noticed in the past two stages of development, mouse events would pass right through our InnerFrames. By constructing a component to capture mouse events and placing it in our rootPane's layeredPane, we can stop this from happening. This is the purpose of CapturePanel. It is a simple JPanel with an empty MouseInputAdapter shell attached as a MouseListener and MouseMotionListener. This adapter will consume any mouse events passed to it. When an InnerFrame is constructed, as we discussed above, a CapturePanel instance is added at the lowest possible layer of its layeredPane. Thus, mouse events that don't get handled by a component in a higher layer, such as its contentPane, will get trapped here.

Running The Code

Note that we've added one line to LayeredPaneDemo in this section that we didn't mention yet:

  frames[i].getContentPane().add(new JScrollPane(new JLabel(ii)));

This places a JScrollPane containing a JLabel with an image in InnerFrame's contentPane.

Figure 15.8 shows LayeredPaneDemo in action. Experiment with maximizing, iconifying and restoring. Drag frames all around the layered pane and ensure that they cannot be lost from view. Now resize a frame and notice that we can lose the title bar if it is resized above our JFrame title bar. (This is a flaw that should be accounted for in any commercial MDI.)

Now try maximizing an InnerFrame and changing the size of the JFrame. You will notice that the maximized InnerFrame does not change size along with its parent. In the next section we show how to implement this as well as other important features.



[ Return to Swing (Book) ]


Top 10 read Java Articles
 Get free "1000 Java Tips eBook"

 Java Calendar and Date: good to know facts and code examples

 Array vs ArrayList vs LinkedList vs Vector: an excellent overview and examples

 How can I convert any Java Object into byte array? And byte array to file object

 The Java Lesson 1: What is Java?

 How do I compare two dates and times, date between dates, time between times and

 Maven vs Ant or Ant vs Maven?

 How to open, read, write, close file(s) in Java? Examples on move, rename and de

 Java Array

 Java: JLabel font and color


[ More in News Section ]
Java Lessons

The Java Lesson 1:
What is Java?
The Java Lesson 2:
Anatomy of a simple Java program
The Java Lesson 3:
Identifiers and primitive data types
The Java Lesson 4:
Variables, constants, and literals
The Java Lesson 5:
Arithmetic operations, conversions, and casts
The Java Lesson 6:
Boolean expressions and operations
The Java Lesson 7:
Bitwise operations
The Java Lesson 8:
Flow control with if and else
The Java Lesson 9:
switch statements
The Java Lesson 10:
for, while, and do-while statements
The Java Lesson 11:
Using break and continue
The Java Lesson 12:
Class methods and how they are called
The Java Lesson 13:
Using the Math class
The Java Lesson 14:
Creating and calling custom class methods
The Java Lesson 15:
Overloading class methods
The Java Lesson 16:
An introduction to objects and object references
The Java Lesson 17:
The String class
The Java Lesson 18:
The StringBuffer class
The Java Lesson 19:
Initializing and processing arrays of primitives
The Java Lesson 20:
Initializing and processing arrays of objects
The Java Lesson 23:
Inheritance and overriding inherited methods
The Java Lesson 24:
abstract classes and polymorphism
The Java Lesson 25:
Interfaces, instanceof, and object conversion and casting
The Java Lesson 26:
Introduction to graphical programming and the java.awt packa
The Java Lesson 27:
The Component class
The Java Lesson 28:
Containers and simple layout managers
The Java Lesson 29:
The Color and Font classes
The Java Lesson 30:
Drawing geometric shapes
The Java Lesson 31:
Choice, List, and Checkbox controls
The Java Lesson 32:
Using the Scrollbar graphical control
The Java Lesson 33:
Menus and submenus
The Java Lesson 34:
An introduction to applets and the Applet class
The Java Lesson 35:
Essential HTML to launch an applet and pass it parameters
The Java Lesson 36:
Mouse event processing
Java Lesson 37:
Menus and submenus
Java Lesson 38:
The WindowListener interface and the WindowAdapter class
Java Lesson 39:
An introduction to GridBagLayout
Java Lesson 40:
An introduction to the Java Collections API
Java Lesson 41:
Exception handling with try, catch, and finally blocks
Java Lesson 42:
Claiming and throwing exceptions
Java Lesson 43:
Multithreading, the Thread class, and the Runnable interface
Java Lesson 44:
An introduction to I/O and the File and FileDialog classes
Java Lesson 45:
Low-level and high-level stream classes
Java Lesson 46:
Using the RandomAccessFile class
Java Lessons by
Joh Huhtala: Update

Latest articles
 Java Profiler JProbe to Resolve Performance Problems Faster

 SSL with GlassFish v2, page 5

 SSL with GlassFish v2, page 4

 SSL with GlassFish v2, page 3

 SSL with GlassFish v2, page 2

 The Java Lesson 2: Anatomy of a simple Java program, page 2

 New site about Java for robots and robotics: both software and hardware.

 Exceptions -III: What's an exception and why do I care?

 Exceptions -II: What's an exception and why do I care?

 Exceptions: What's an exception and why do I care?

 Double your Java code quality in 10 minutes, here is receipt

 Murach's Java Servlets and JSP

 How to get ascii code from a char in Java?

 Can we just try without catch? Yes!

 Make Tomcat page load faster

 Make your Tomcat More secure - limit network address for certain IP addresses

 New Java book online starts now here...

 Implementing RESTful Web Services in Java

 Firefox trimming from 1 GB to 40 Mb with many tabs opened

 SSL with GlassFish v2

 My request to replublish Tech Tips

 Search JavaFAQ.nu site here

 New Advanced Installer for Java 6.0 brings XML updates and imports 3rd party MSI

 EJB programming restrictions

 Maven vs Ant or Ant vs Maven?

 Why Java does not use default value which it should?

 How to unsign signed bytes in Java - your guide is here

 The Java Lesson 3: Identifiers and primitive data types. Page 2

 The Java Lesson 7: Bitwise operations with good examples, click here! Page 4

 The Java Lesson 7: Bitwise operations with good examples, click here! Page 3


[ More in News Section ]


Home Code Examples Java Forum All Java Tips Books Submit News, Code... Search... Offshore Software Tech Doodling

RSS feed Java FAQ RSS feed Java FAQ News     

    RSS feed Java Forums RSS feed Java Forums

All logos and trademarks in this site are property of their respective owner. The comments are property of their posters, all the rest 1999-2006 by Java FAQs Daily Tips.

Interactive software released under GNU GPL, Code Credits, Privacy Policy